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