// 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.Diagnostics; using System.Runtime.Serialization; using System.Collections; using System.Collections.Immutable; using System.ComponentModel; namespace OpenTap { /// /// Abstract class forming the basis for all ResultListeners. /// public abstract class ResultListener : Resource, IResultListener, IEnabledResource { bool isEnabled = true; /// Gets or sets if this resource is enabled. [Browsable(false)] public bool IsEnabled { get => isEnabled; set { var oldValue = isEnabled; isEnabled = value; onEnabledChanged(oldValue, value); } } /// Called when IsEnabled is changed. /// /// protected virtual void onEnabledChanged(bool oldValue, bool newValue) { } /// /// Called when a test plan starts. /// /// Test plan run parameters. public virtual void OnTestPlanRunStart(TestPlanRun planRun) { } /// /// Called when test plan finishes. At this point no more results will be sent to the result listener from the test plan run. /// /// Test plan run parameters. /// The log file from the test plan run as a stream. public virtual void OnTestPlanRunCompleted(TestPlanRun planRun, System.IO.Stream logStream) { } /// /// Called just before a test step is started. /// /// public virtual void OnTestStepRunStart(TestStepRun stepRun) { } /// /// Called when a test step run is completed. /// Result might still be propagated to the result listener after this event. /// /// Step run parameters. public virtual void OnTestStepRunCompleted(TestStepRun stepRun) { } /// /// Called when a result is received. /// /// Step run ID. /// Result structure. public virtual void OnResultPublished(Guid stepRunId, ResultTable result) { } // lookup keeping track of which result listeners implements OnResultPublished static ImmutableDictionary implementsOnResultsPublished = ImmutableDictionary.Empty; /// Returns true if the result listener has implemented OnResultPublished. This is used to optimize performance in situations where they don't. internal static bool ImplementsOnResultsPublished(ResultListener resultListener) { var resultListenerType = resultListener.GetType(); if (!implementsOnResultsPublished.TryGetValue(resultListenerType, out bool doesImplement)) { doesImplement = resultListenerType.MethodOverridden(typeof(ResultListener), nameof(OnResultPublished)); implementsOnResultsPublished = implementsOnResultsPublished.Add(resultListenerType, doesImplement); } return doesImplement; } } /// /// Instructs the ResultListener not to save the /// public property value as metadata for TestStep results. /// [Obsolete("This attribute is no longer in use and will be removed in a later version.")] [AttributeUsage(AttributeTargets.Property)] public class ResultListenerIgnoreAttribute : Attribute { } /// /// Represents a result parameter. /// [DebuggerDisplay("{Group} {Name} = {Value}")] [DataContract] public class ResultParameter : IParameter { /// /// Name of parameter. /// [DataMember] public readonly string Name; /// /// Group name of the parameter. /// [DataMember] public readonly string Group; /// /// Value of the parameter. If null, the value is the string "NULL". /// [DataMember] public readonly IConvertible Value; /// /// Indicates the parameter came from a test step in a parent level above the initial object. /// [DataMember] public readonly int ParentLevel; internal (string, string) Key => (Name, Group); IConvertible IParameter.Value => Value; string IAttributedObject.Name => Name; string IParameter.Group => Group; string IAttributedObject.ObjectType => "Parameter"; /// Gets if this result is metadata. public bool IsMetaData { get; } /// null or the macro name representation of the ResultParameter. This will make it possible to insert the parameter value into a string. public readonly string MacroName; /// Creates a result parameter with default group. public ResultParameter(string name, IConvertible value) { Name = name; Value = value ?? "NULL"; Group = ""; MacroName = null; } /// /// Initializes a new instance of ResultParameter. /// public ResultParameter(string group, string name, IConvertible value, MetaDataAttribute metadata = null, int parentLevel = 0) { Group = group ?? ""; Name = name; Value = value ?? "NULL"; ParentLevel = parentLevel; if (metadata != null) { MacroName = metadata.MacroName; IsMetaData = true; } else { MacroName = null; } } /// Creates a new ResultParameter. /// /// /// /// public ResultParameter(string group, string name, IConvertible value, string macroName) { Group = group; Name = name; Value = value ?? "NULL"; if (macroName != null) { IsMetaData = true; if (macroName.Length > 0) MacroName = macroName; else MacroName = name; } } /// /// Determines whether the specified object is equal to the current object. /// /// /// public override bool Equals(object obj) { var parameter = obj as ResultParameter; return parameter != null && Name == parameter.Name && Group == parameter.Group && EqualityComparer.Default.Equals(Value, parameter.Value) && ParentLevel == parameter.ParentLevel && MacroName == parameter.MacroName; } /// /// Calculates a hash code for the current object. /// /// public override int GetHashCode() { var hashCode = -1808396095; hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Name); hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Group); hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Value); hashCode = hashCode * -1521134295 + ParentLevel.GetHashCode(); hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(MacroName); return hashCode; } } // this array can be indexed and resized at the same time. class SafeArray : IEnumerable { T[][] arrays = Array.Empty(); int count; public int Count => count; public SafeArray(T[] init) { arrays = new[] {init}; count = init.Length; } public SafeArray(SafeArray original) { count = original.count; arrays = new []{new T[count]}; int offset = 0; foreach (var elems in original.arrays) { elems.CopyTo(arrays[0], offset); offset += elems.Length; } } public SafeArray() { } readonly object resizeLock = new object(); public void Resize(int newSize) { if(newSize < count) throw new InvalidOperationException(); if (newSize == count) return; lock (resizeLock) { Array.Resize(ref arrays, arrays.Length + 1); arrays[arrays.Length - 1] = new T[newSize - count]; count = newSize; } } public ref T this[int index] { get { for(int i = 0; i < arrays.Length; i++) { var array = arrays[i]; if (index < array.Length) return ref array[index]; index -= array.Length; } throw new IndexOutOfRangeException(); } } public IEnumerator GetEnumerator() => arrays.SelectMany(x => x).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } /// /// A collection of parameters related to the results. /// public class ResultParameters : IReadOnlyList { struct resultParameter { public string Name; public IConvertible Value; public string Group; public string MacroName; public ResultParameter ToResultParameter() => new ResultParameter(Group, Name, Value, MacroName); public static implicit operator resultParameter(ResultParameter v) { return new resultParameter { Name = v.Name, Value = v.Value, Group = v.Group, MacroName = v.MacroName ?? (v.IsMetaData ? "" : null) }; } } SafeArray data = new SafeArray(); /// /// Gets the parameter with the given index. /// public ResultParameter this[int index] => data[index].ToResultParameter(); /// Gets a ResultParameter by name. public ResultParameter Find(string name) => Find(name, ""); /// Gets a ResultParameter by name. public ResultParameter Find(string name, string group) { if (indexByName.TryGetValue((name, group), out var idx) == false) return null; return this[idx]; } /// Gets a ResultParameter by name. public ResultParameter Find((string name, string group) key) { if (indexByName.TryGetValue(key, out var idx) == false) return null; return this[idx]; } int FindIndex((string name, string group) key) { if (indexByName.TryGetValue(key, out var idx)) return idx; return -1; } /// Gets the parameter with the key name. public IConvertible this[string name, string group] { get => Find(name, group)?.Value; set { int index = -1; SetIndexed((name, group), ref index, value); } } /// Gets a named parameter specifying only name. This assumes that the empty group is being used. So it is the same as calling [name, ""].. public IConvertible this[string name] { get => this[name, ""]; set => this[name, ""] = value; } static void getMetadataFromObject(object res, string nestedName, ICollection output) { GetPropertiesFromObject(res, output, nestedName, true); } /// /// Returns a list with one entry for every property on the inputted /// object decorated with . /// public static ResultParameters GetMetadataFromObject(object res) { if (res == null) throw new ArgumentNullException(nameof(res)); var parameters = new List(); getMetadataFromObject(res, "", parameters); return new ResultParameters(parameters); } /// /// Returns a list with one entry for every property on every /// implementation decorated with . /// public static ResultParameters GetComponentSettingsMetadata(bool expandComponentSettings = false) { var componentSettings = TypeData.FromType(typeof(ComponentSettings)) //get component settings instances (lazy) .DerivedTypes .Where(x => x.CanCreateInstance && ParameterCache.GetParametersMap(x, true).Any()) .Select(td => ComponentSettings.GetCurrent(td.Type)) .Where(o => o != null) .Cast(); if (expandComponentSettings) { var componentSettingsLists = TypeData .FromType(typeof(ComponentSettings)) //get component settings instances (lazy) .DerivedTypes .Where(x => x.CanCreateInstance && x.DescendsTo(typeof(IEnumerable))) .Select(td => ComponentSettings.GetCurrent(td.Type)) .Where(o => o != null) .Cast(); if (expandComponentSettings) componentSettings = componentSettings.Concat(componentSettingsLists.OfType().SelectMany(c => (IEnumerable)c)).Distinct(); } return new ResultParameters(componentSettings.ToArray().SelectMany(GetMetadataFromObject).ToArray());// get metadata from each. } /// /// Lazily pull result parameters from component settings. Reduces the number of component settings XML that needs to be deserialized. /// /// If objects in ComponentSettingsLists should be included. /// internal static IEnumerable GetComponentSettingsMetadataLazy(bool includeObjects) { TypeData engineSettingsType = TypeData.FromType(typeof(EngineSettings)); AssemblyData engineAssembly = engineSettingsType.Assembly; int orderer(TypeData tp) { // always start with EngineSettings // prefer engine assemblies // then loaded assemblies // then everything else. if (tp == engineSettingsType) return 3; if (tp.Assembly == engineAssembly) return 2; if (tp.Assembly.Status == LoadStatus.Loaded) return 1; return 0; } var types = TypeData.FromType(typeof(ComponentSettings)) .DerivedTypes.OrderByDescending(orderer).ToArray(); foreach (var tp in types) { var t = tp.Load(); if (tp.CanCreateInstance == false) continue; if (ParameterCache.GetParametersMap(tp, true).Any() == false) continue; var componentSetting = ComponentSettings.GetCurrent(t); if (componentSetting != null) { yield return GetMetadataFromObject(componentSetting); } } if (includeObjects == false) yield break; foreach (var tp in types) { var t = tp.Load(); if (tp.CanCreateInstance == false) continue; if (tp.DescendsTo(typeof(IEnumerable)) == false) continue; var elements = ComponentSettings.GetCurrent(t) as IEnumerable ?? Array.Empty(); foreach (var elem in elements) yield return GetMetadataFromObject(elem); } } /// /// Adds a new parameter to the resultParams list. if the parameter value is of the type Resource, every parameter from it is added, but not the origin object. /// static void GetParams(string group, string name, object value, MetaDataAttribute metadata, ICollection output) { if (value is IResource) { string resval = value.ToString(); output.Add(new ResultParameter(group, name, resval, metadata)); getMetadataFromObject(value, name + "/",output); return; } var parentName = name; IConvertible val; if (value == null) val = null; else if (value is IConvertible conv) val = conv; else if((val = StringConvertProvider.GetString(value)) == null) val = value.ToString(); output.Add( new ResultParameter(group, parentName, val, metadata)); } internal class ParameterCache { class Box { public ImmutableDictionary Data = ImmutableDictionary.Empty; } static readonly ThreadField cacheField = new ThreadField(); public static void LoadCache() => cacheField.Value = new Box(); public static IEnumerable<(IMemberData member, string group, string name, MetaDataAttribute metadata)> GetParametersMap(ITypeData type, bool metadataOnly) { if (cacheField.Value?.Data.TryGetValue(type, out var data) == true) { if (metadataOnly) return data.Where(x => x.metadata != null); return data; } var value = getParametersMap(type, false); if (cacheField.Value is Box box) { box.Data = box.Data.SetItem(type, value.ToArray()); } if (metadataOnly) return value.Where(x => x.metadata != null); return value; } static IEnumerable<(IMemberData member, string group, string name, MetaDataAttribute metadata)> getParametersMap(ITypeData type, bool metadataOnly) { foreach (var prop in type.GetMembers()) { if (!prop.Readable) continue; var metadataAttr = prop.GetAttribute(); if (metadataAttr == null) { if (metadataOnly) continue; // if metadataAttr is specified, all we require is that we can read and write it. // Otherwise normal rules applies: if (prop.Writable == false) continue; // Don't add Properties with XmlIgnore attribute if (prop.HasAttribute()) continue; if (!prop.IsBrowsable()) continue; } if (prop.HasAttribute()) continue; // pretty rare case, best to check this after other things for performance. var display = prop.GetDisplayAttribute(); if (metadataAttr != null && string.IsNullOrWhiteSpace(metadataAttr.MacroName)) metadataAttr = new MetaDataAttribute(metadataAttr.PromptUser, display.Name) { Group = metadataAttr.Group, Name = metadataAttr.Name }; var name = display.Name.Trim(); string group = ""; if (display.Group.Length == 1) group = display.Group[0].Trim(); group = metadataAttr?.Group ?? group; name = metadataAttr?.Name ?? name; yield return (prop, group, name, metadataAttr); } } } static void GetPropertiesFromObject(object obj, ICollection output, string namePrefix = "", bool metadataOnly = false, ITypeData type = null) { if (obj == null) return; type ??= TypeData.GetTypeData(obj); foreach (var (prop, group, name, metadata) in ParameterCache.GetParametersMap(type, metadataOnly)) { object value = prop.GetValue(obj); if (value == null) continue; GetParams(group, namePrefix + name, value, metadata, output); } } internal static void UpdateParams(ResultParameters parameters, object obj, string namePrefix = "", ITypeData type = null) { if (obj == null) return; type ??= TypeData.GetTypeData(obj); var p = ParameterCache.GetParametersMap(type, false); foreach (var (prop, group, _name, metadata) in p) { if (prop.GetAttribute()?.Frozen == true) continue; var name = namePrefix + _name; object value = prop.GetValue(obj); if (value == null) continue; if (value is IResource) { string resval = value.ToString(); parameters.Overwrite(name, resval, group, metadata); UpdateParams(parameters, value, name); continue; } IConvertible val = value as IConvertible ?? StringConvertProvider.GetString(value) ?? value.ToString(); parameters.Overwrite(name, val, group, metadata); } } internal void Overwrite(string name, IConvertible value, string group, MetaDataAttribute metadata) { Add(group, name, value, metadata); } /// /// Returns a list with one entry for every setting of the inputted /// TestStep. /// internal static ResultParameters GetParams(ITestStep step, ITypeData type = null) { if (step == null) throw new ArgumentNullException(nameof(step)); var parameters = new List(5); GetPropertiesFromObject(step, parameters, type: type); if (parameters.Count == 0) return new ResultParameters(); return new ResultParameters(parameters); } /// /// Initializes a new instance of the ResultParameters class. /// public ResultParameters() { indexByName = new Dictionary<(string, string), int>(); } /// /// Initializes a new instance of the ResultParameters class. /// public ResultParameters(IEnumerable items) { addRangeUnsafe(items, true); } /// /// Returns a dictionary containing all the values in this list indexed by their . /// /// public Dictionary ToDictionary() { Dictionary values = new Dictionary(); foreach (ResultParameter val in this) { if(values.ContainsKey(val.Name) == false) values.Add(val.Name, val.Value); } return values; } /// /// Returns the number of result parameters. /// public int Count => count; /// Adds a new element to the parameters. (synchronized). public void Add(ResultParameter parameter) => AddRange(new[] {parameter}); int count = 0; void addRangeUnsafe(IEnumerable parameters, bool initCollection = false) { if (initCollection) { var capacity = parameters.Count(); var data2 = new resultParameter[capacity]; var indexByName2 = new Dictionary<(string, string), int>(capacity); var count2 = 0; foreach (var par in parameters) { if (par?.Name == null) continue; if (indexByName2.TryGetValue(par.Key, out var idx)) { data2[idx] = par; } else { indexByName2[par.Key] = count2; data2[count2] = par; count2 += 1; } } // resize data to the actual count. This is a no-op if no change. Array.Resize(ref data2, count2); data = new SafeArray(data2); indexByName = indexByName2; count = count2; return; } Dictionary<(string, string), int> newIndexes = null; List newParameters = null; foreach (var par in parameters) { if (!indexByName.TryGetValue(par.Key, out var idx)) idx = -1; if (idx >= 0) { data[idx] = par; continue; } if (newIndexes == null) { newIndexes = new Dictionary<(string, string), int>(); newParameters = new List(); } if (newIndexes.TryGetValue(par.Key, out idx)) { newParameters[idx - count] = par; } else { int nidx = count + newParameters.Count; newParameters.Add(par); newIndexes[par.Key] = nidx; } } if (newIndexes != null) { var newSize = count + newParameters.Count; if(data.Count < newSize) data.Resize(newSize); foreach (var elem in newParameters) { data[count] = elem; count += 1; } foreach (var kv in newIndexes) indexByName.Add(kv.Key, kv.Value); } } Dictionary<(string, string), int> indexByName; readonly object addLock = new object(); /// /// Adds a range of result parameters (synchronized). /// /// public void AddRange(IEnumerable parameters) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); lock(addLock) addRangeUnsafe(parameters); } IEnumerator IEnumerable.GetEnumerator() => data.Select(x => x.ToResultParameter()).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => data.Select(x => x.ToResultParameter()).GetEnumerator(); /// Copies all the data inside a ResultParameters instance. /// internal ResultParameters Clone() { var r = new ResultParameters { data = new SafeArray(data), indexByName = new Dictionary<(string, string), int>(indexByName) }; return r; } internal IConvertible GetIndexed((string, string) index, ref int keyIndex) { if(keyIndex == -1) keyIndex = FindIndex(index); if (keyIndex == -1) return null; return data[keyIndex].Value; } internal void SetIndexed((string name, string group) key, ref int index, IConvertible value) { if(index == -1) index = FindIndex(key); if (index == -1) { Add(new ResultParameter(key.group, key.name, value)); } else { var prev = data[index]; prev.Value = value; prev.Name = key.name; prev.Group = key.group; data[index] = prev; } } internal void IncludeMetadataFromObject(TestPlan obj) { var metadata = GetMetadataFromObject(obj); AddRange(metadata); } /// Adds a new result parameter. public void Add(string group, string name, IConvertible value, MetaDataAttribute metaDataAttribute) { Add(new ResultParameter(group, name, value, metaDataAttribute)); } } }