// 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; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace OpenTap { /// /// A class to store a column of data for a . /// [Serializable] public class ResultColumn : IResultColumn, IData { /// /// The name of the column. /// public string Name { get; private set; } /// /// The data in the column. /// public Array Data { get; private set; } /// /// The TypeCode of data in the column. /// public TypeCode TypeCode { get; private set; } /// /// String describing the column. /// public string ObjectType { get; } /// /// Helper to access a strongly typed value in the array. /// /// /// Index in the array. /// public T GetValue(int Index) where T : IConvertible { if ((Index < 0) || (Index >= Data.Length)) return default; var value = Data.GetValue(Index); if (value == null) return default; if (typeof(T) == typeof(object)) return (T)value; return (T)Convert.ChangeType(value, Type.GetTypeCode(typeof(T))); } /// /// Creates a new populated result column. /// /// The name of the column. /// The data of the column. public ResultColumn(string name, Array data) { if (name == null) throw new ArgumentNullException(nameof(name)); if (data == null) throw new ArgumentNullException(nameof(data)); Name = name; Data = data; TypeCode = Type.GetTypeCode(data.GetType().GetElementType()); Parameters = ParameterCollection.Empty; ObjectType = ResultObjectTypes.ResultColumn; } /// Creates a new result column with parameters attached. public ResultColumn(string name, Array data, params IParameter[] parameters) : this(name, data) { Parameters = new ParameterCollection(parameters); } internal ResultColumn(string name, Array data, IData table, IParameters parameters, string ObjectType = ResultObjectTypes.ResultColumn) : this(name, data) { Parameters = parameters; Parent = table; this.ObjectType = ObjectType; } internal ResultColumn WithResultTable(ResultTable table) { return new ResultColumn(Name, Data, table, Parameters, (this as IAttributedObject).ObjectType); } /// /// The parent object of this column. Usually a Result Table. This value will get assigned during ResultProxy.Publish. /// public IData Parent { get; } /// Unused. long IData.GetID() => 0; /// The parameters attached to this column. public IParameters Parameters { get; } /// Create a result column clone with additional parameters. public ResultColumn AddParameters(params IParameter[] additionalParameters) { return new ResultColumn(Name, Data, Parent, new ParameterCollection(Parameters.Concat(additionalParameters).ToArray())); } } /// /// A result table containing rows of results with matching names, column name, and types. /// [Serializable] public class ResultTable : IResultTable { /// /// The name of the results. /// public string Name { get; private set; } ResultColumn[] columns; /// An array containing the result columns. public ResultColumn[] Columns { get => columns; private set => columns = value; } /// /// Indicates how many rows of results this table contains. /// public int Rows { get; private set; } IResultColumn[] IResultTable.Columns { get { return Columns; } } /// /// The parent of this object. /// public IData Parent { get; protected set; } /// /// Parameters attached to this Result Table. /// Note, test step parameter are often attached in the result listener and does not need to be added here. /// public IParameters Parameters { get; } = ParameterCollection.Empty; string IAttributedObject.ObjectType => ResultObjectTypes.ResultVector; long IData.GetID() { return 0; } /// /// Creates an empty results table. /// public ResultTable() { Name = ""; Columns = new ResultColumn[0]; Rows = 0; } /// /// Creates a new result table. /// /// The name of the result table. /// The columns of the table. public ResultTable(string name, ResultColumn[] resultColumns) { if (name == null) throw new ArgumentNullException(nameof(name)); if (resultColumns == null) throw new ArgumentNullException(nameof(resultColumns)); Name = name; columns = resultColumns.ToArray(); for (int i = 0; i < columns.Length; i++) { columns[i] = columns[i].WithResultTable(this); } if (columns.Length <= 0) Rows = 0; else { Rows = columns[0].Data.Length; for (int i = 1; i < columns.Length; i++) { if (columns[i].Data.Length != Rows) throw new ArgumentException("Columns needs to be of same length.", nameof(resultColumns)); } } } /// /// Creates a new Result Table with a name, result columns and parameters. /// public ResultTable(string name, ResultColumn[] resultColumns, params IParameter[] parameters) : this(name, resultColumns) { Parameters = new ParameterCollection(parameters); } ResultTable(string name, ResultColumn[] resultColumns, IParameters parameters) : this(name, resultColumns) { Parameters = parameters; } internal ResultTable WithName(string newName) => new ResultTable(newName, Columns, Parameters); } /// /// Interface that the TestStep can access through the Results property. /// public interface IResultSource { /// /// Waits for the propagation of results from all Proxies to the Listeners. Normally this is not necessary. /// However, if a step needs to change a property after it has written results, this method makes sure the ResultListeners record the previous/correct value before changing it. /// void Wait(); /// /// Defer an action from the current teststep run. /// This action will be called as soon as possible, and block the execution for any parent steps. /// /// void Defer(Action action); /// /// Run an action as the final step after the last deferred action. /// This should not be used while the associated TestStep is running. /// /// void Finally(Action action); /// /// Stores a result. These results will be propagated to the ResultStore after the TestStep completes. /// /// Name of the result. /// Titles of the columns. /// The values of the results to store. void Publish(string name, List columnNames, params IConvertible[] results); /// /// The fastest way to store a result. These results will be propagated to the ResultStore after the TestStep completes. /// /// Name of the result. /// Titles of the columns. /// The columns of the results to store. /// /// This is the fastest way to store a large number of results. /// void PublishTable(string name, List columnNames, params Array[] results); } /// /// Temporarily holds results from a TestStep, before they are propagated to the ResultListener by the TestPlan. See /// public class ResultSource : IResultSource { /// Logging source for this class. static readonly TraceSource log = Log.CreateSource("ResultProxy"); static Array GetArray(Type type, object Value) { var array = Array.CreateInstance(type, 1); array.SetValue(Value, 0); return array; } void Propagate(IResultListener rt, ResultTable result) { try { rt.OnResultPublished(stepRun.Id, result); } catch (Exception e) { log.Warning("Caught exception in result handling task."); log.Debug(e); planRun.RemoveFaultyResultListener(rt); } } /// /// The current plan run. /// readonly TestPlanRun planRun; /// /// The TestStepRun for this object. /// readonly TestStepRun stepRun; /// /// Adds an additional parameter to this TestStep run. /// /// Parameter to add. public void AddParameter(ResultParameter param) { stepRun.Parameters.Add(param); } /// /// Creates a new ResultProxy. Done for each test step run. /// /// TestStepRun that this result proxy is proxy for. /// TestPlanRun that this result proxy is proxy for. public ResultSource(TestStepRun stepRun, TestPlanRun planRun) { this.stepRun = stepRun; this.planRun = planRun; stepRun.SetResultSource(this); } /// /// Waits for the propagation of results from all Proxies to the Listeners. Normally this is not necessary. /// However, if a step needs to change a property after it has written results, this method makes sure the ResultListeners record the previous/correct value before changing it. /// public void Wait() { planRun.WaitForResults(); } WorkQueue deferWorker; List deferExceptions; /// /// Defer an action from the current test step run. This means the action will be executed some time after /// the current run. /// /// public void Defer(Action action) { if (TapThread.Current != stepRun.StepThread) throw new InvalidOperationException( "Defer may only be executed from the same thread as the test step."); DeferNoCheck(action); } int deferCount = 0; internal void DeferNoCheck(Action action) { if (deferWorker == null) { deferExceptions = new List(); deferWorker = new WorkQueue(WorkQueue.Options.None, "Defer Worker"); } Interlocked.Increment(ref deferCount); // only one defer task may run at a time. deferWorker.EnqueueWork(() => { try { action(); } catch (Exception e) { deferExceptions.Add(e); } finally { Interlocked.Decrement(ref deferCount); } }); } static readonly Task Finished = Task.FromResult(0); /// /// Run an action as the final step after the last deferred action /// /// void IResultSource.Finally(Action action) { if (deferCount == 0) { action(Finished); deferWorker?.Dispose(); } else { deferWorker.EnqueueWork(() => { try { if (deferExceptions.Count == 1) action(Task.FromException(deferExceptions[0])); else if (deferExceptions.Count > 1) action(Task.FromException(new AggregateException(deferExceptions.ToArray()))); else action(Finished); deferWorker.Dispose(); } catch (OperationCanceledException) { } catch (Exception e) { log.Error("Caught error while finalizing test step run. {0}", e.Message); log.Debug(e); } }); } } Dictionary> resultFunc = null; //new Dictionary>(); readonly object resultFuncLock = new object(); ResultTable ToResultTable(T result) { ITypeData runtimeType = TypeData.GetTypeData(result); if (resultFunc == null) { lock (resultFuncLock) resultFunc = new Dictionary>(); } if (!resultFunc.ContainsKey(runtimeType)) { bool enumerable = result is IEnumerable && !(result is string); var targetType = runtimeType; if (enumerable) { targetType = targetType.AsTypeData().ElementType; } var Typename = targetType.GetDisplayAttribute().GetFullName(); var classParameters = targetType.GetAttributes().ToArray(); var Props = targetType.GetMembers() .Where(x => x.Readable && x.TypeDescriptor.DescendsTo(typeof(IConvertible))).ToArray(); var PropNames = Props.Select(p => p.GetDisplayAttribute().GetFullName()).ToArray(); var propParameter = Props.Select(p => p.GetAttributes().ToArray()).ToArray(); resultFunc[runtimeType] = (v) => { int count; IEnumerable values; if (enumerable) { values = (IEnumerable)v; count = values.Count(); } else { values = new[] { v }; count = 1; } var arrays = Props .Select(p => Array.CreateInstance(p.TypeDescriptor.AsTypeData().Type, count)).ToArray(); int j = 0; foreach (var obj in values) { for (int i = 0; i < Props.Length; i++) { arrays[i].SetValue(Props[i].GetValue(obj), j); } j++; } var cols = new ResultColumn[Props.Length]; for (int i = 0; i < Props.Length; i++) { cols[i] = new ResultColumn(PropNames[i], arrays[i], propParameter[i]); } return new ResultTable(Typename, cols, classParameters); }; } return resultFunc[runtimeType](result); } /// /// Stores an object as a result. These results will be propagated to the ResultStore after the TestStep completes. /// /// /// The result whose properties should be stored. public void Publish(T result) { if (result == null) throw new ArgumentNullException(nameof(result)); PublishTable(ToResultTable(result)); } /// Publishes a result table. public void Publish(ResultTable result) => PublishTable(result); /// /// Stores an object as a result. These results will be propagated to the ResultStore after the TestStep completes. /// /// /// The name of the result. /// The result whose properties should be stored. public void Publish(string name, T result) { if (result == null) throw new ArgumentNullException(nameof(result)); if (name == null) throw new ArgumentNullException(nameof(name)); Publish(ToResultTable(result).WithName(name)); } /// /// Stores a result. These results will be propagated to the ResultStore after the TestStep completes. /// /// Name of the result. /// Titles of the columns. /// The values of the results to store. public void Publish(string name, List columnNames, params IConvertible[] results) { if (columnNames == null) throw new ArgumentNullException(nameof(columnNames)); if (results == null) throw new ArgumentNullException(nameof(results)); var columns = results.Zip(columnNames, (val, title) => new ResultColumn(title, GetArray(val == null ? typeof(object) : val.GetType(), val))).ToArray(); Publish(new ResultTable(name, columns)); } /// /// The fastest way to store a result. These results will be propagated to the ResultStore after the TestStep completes. /// /// Name of the result. /// Titles of the columns. /// The columns of the results to store. /// /// This is the fastest way to store a large number of results. /// public void PublishTable(string name, List columnNames, params Array[] results) { if (columnNames == null) throw new ArgumentNullException(nameof(columnNames)); if (results == null) throw new ArgumentNullException(nameof(results)); var columns = results.Zip(columnNames, (val, title) => new ResultColumn(title, val)).ToArray(); Publish(new ResultTable(name, columns)); } /// Publishes a result table. public void PublishTable(ResultTable table) { planRun.ScheduleInResultProcessingThread(new PublishResultTableInvokable(table, this)); } internal bool WasDeferred => deferWorker != null; class PublishResultTableInvokable : IInvokable, ISkippableInvokable { readonly ResultTable table; readonly ResultSource proxy; public PublishResultTableInvokable(ResultTable table, ResultSource proxy) { this.table = table; this.proxy = proxy; } /// /// If possible, introspect the current work queue and collapse result table propagations into one. /// This can give a huge performance boost for many use cases, but mostly when PublishTable is not used. /// /// Note that this has to be done in the result listener thread - since each may have different number of elements queued /// depending on the speed of the result listener. Slow ones like internet based ones will have more items queued. /// /// An optimized table or the original one if it is not possible to optimize. ResultTable CreateOptimizedTable(WorkQueue workQueue) { // optimization: only allocate the list if there are more than one mergeable table. List mergeTables = null; while (workQueue?.Peek() is PublishResultTableInvokable p) { // this can occur if two steps are publishing results in parallel. // in this case, the tables should not be combined. if(p.proxy != proxy) break; // check if the tables can be merged. if (!ResultTableOptimizer.CanMerge(p.table, table) ) break; // pop the peeked object var dq = workQueue.Dequeue(); if (dq != p) { if (dq == null) // we weren't able to dequeue the object - just give up on the optimization. break; // This should never happen. // If it did, something went really wrong. Debug.Fail("Peeked object not in front of queue."); throw new InvalidOperationException("Peeked object not in front of queue."); } // optimization: only allocate a list if it helps. Also initialize it two long to avoid more allocations. if (mergeTables == null) mergeTables = new List{table, p.table}; else mergeTables.Add(p.table); } if (mergeTables != null) return ResultTableOptimizer.MergeTables(mergeTables); return table; } public void Invoke(IResultListener a, WorkQueue queue) { try { // only merge result tables where it is explicitly supported by the result listener. if (a is IMergedTableResultListener) { a.OnResultPublished(proxy.stepRun.Id, CreateOptimizedTable(queue)); } else { a.OnResultPublished(proxy.stepRun.Id, table); } } catch (Exception e) { log.Warning("Caught exception in result handling task."); log.Debug(e); proxy.planRun.RemoveFaultyResultListener(a); } } // Skip the invocation if the result listener does not implement OnResultPublished. public bool Skip(IResultListener a, WorkQueue b) { if (a is ResultListener r) return ResultListener.ImplementsOnResultsPublished(r) == false; return false; } } } }