using System; using System.Collections.Generic; using System.Linq; namespace OpenTap { /// Accelerating structure for inputs / outputs owners. Note, it is recommended to implement this explicitly. interface IInputOutputRelations { /// Relations to the object ('this'). InputOutputRelation[] Inputs { get; set; } /// Relations from the object('this'); InputOutputRelation[] Outputs { get; set; } } /// Relations between two object and a pair of their members. public sealed class InputOutputRelation { readonly ITestStepParent outputObject; readonly ITestStepParent inputObject; readonly IMemberData outputMember; readonly IMemberData inputMember; /// The object that owns the output member. public ITestStepParent OutputObject => outputObject; /// The object that owns the input member. public ITestStepParent InputObject => inputObject; /// The member which the output value is read from. public IMemberData OutputMember => outputMember; /// The Member which the input value is read from. public IMemberData InputMember => inputMember; InputOutputRelation(ITestStepParent inputObject, IMemberData inputMember, ITestStepParent outputObject, IMemberData outputMember) { this.outputObject = outputObject; this.inputObject = inputObject; this.outputMember = outputMember; this.inputMember = inputMember; } /// Returns true if member on object is assigned to an output / is an input. public static bool IsInput(ITestStepParent @object, IMemberData member) => GetOutputRelations(@object).Any(con => con.InputMember == member && con.InputObject == @object); /// Returns true if member on object is assigned to an input / is an output. public static bool IsOutput(ITestStepParent @object, IMemberData member) => GetInputRelations(@object).Any(con => con.OutputMember == member && con.OutputObject == @object); /// Create a relation between two members on two different objects. public static void Assign(ITestStepParent inputObject, IMemberData inputMember, ITestStepParent outputObject, IMemberData outputMember) { if (outputMember == null) throw new ArgumentNullException(nameof(outputMember)); if (inputMember == null) throw new ArgumentNullException(nameof(inputMember)); if (inputObject == null) throw new ArgumentNullException(nameof(inputObject)); if (outputObject == null) throw new ArgumentNullException(nameof(outputObject)); if (inputMember == outputMember && inputObject == outputObject) throw new ArgumentException("An input/output may not be assigned to itself."); if (outputMember.Readable == false) throw new ArgumentException(nameof(outputMember) + " is not readable!", nameof(outputMember)); if (inputMember.Writable == false) throw new ArgumentException(nameof(inputMember) + " is not writeable!", nameof(inputMember)); var connTo = getOutputRelations(inputObject).ToList(); var connFrom = getInputRelations(outputObject).ToList(); foreach (var connection in connTo) if (connection.OutputObject == outputObject && connection.OutputMember == outputMember && inputMember == connection.InputMember) throw new ArgumentException("Input already assigned", nameof(inputMember)); if (IsInput(inputObject, inputMember)) throw new Exception("This input is already in use by another connection."); // these two restrictions might not be necessary, but it might be good to leave it // in case we want to change the mechanism if (IsInput(outputObject, outputMember)) throw new Exception("An output cannot also be an input"); if (IsOutput(inputObject, inputMember)) throw new Exception("An input cannot also be an output"); var newSpec = new InputOutputRelation(inputObject, inputMember, outputObject, outputMember); connTo.Add(newSpec); connFrom.Add(newSpec); SetOutputRelations(inputObject, connTo.ToArray()); SetInputRelations(outputObject, connFrom.ToArray()); } /// Unassign an input/output relation. /// internal static void Unassign(InputOutputRelation relation) { SetOutputRelations(relation.InputObject, getOutputRelations(relation.InputObject).Where(x => x != relation).ToArray()); SetInputRelations(relation.OutputObject, getInputRelations(relation.OutputObject).Where(x => x != relation).ToArray()); } /// Unassign an input/output relation . public static void Unassign(ITestStepParent target, IMemberData targetMember, ITestStepParent source, IMemberData sourceMember) { var from = getInputRelations(source); var con = from.FirstOrDefault(i => i.InputObject == target && i.InputMember == targetMember && i.OutputMember == sourceMember); if (con != null) Unassign(con); } static bool IsInScope(ITestStepParent target, ITestStep otherStep) { if (target.Parent == otherStep) return true; var parent = target; while (parent != null) { if (parent == otherStep.Parent) return true; parent = parent.Parent; } return false; } static void checkRelations(ITestStepParent target) { Action defer = null; foreach (var connection in getOutputRelations(target)) { if (connection.OutputObject is ITestStep otherStep) { // steps can only be connected to a step from the same test plan. if (IsInScope(target, otherStep) == false) { defer = defer.Bind(Unassign, connection); } } if (connection.OutputMember is IDynamicMemberData dyn && dyn.IsDisposed) { defer = defer.Bind(Unassign, connection); } else if (connection.InputMember is IDynamicMemberData dyn2 && dyn2.IsDisposed) { defer = defer.Bind(Unassign, connection); } } foreach (var connection in getInputRelations(target)) { if (connection.InputMember is IDynamicMemberData dyn && dyn.IsDisposed) { defer = defer.Bind(Unassign, connection); } else if (connection.OutputMember is IDynamicMemberData dyn2 && dyn2.IsDisposed) { defer = defer.Bind(Unassign, connection); } } defer?.Invoke(); } static TestStepRun ResolveStepRun(ITestStep step, Guid waiterStep) { TestStepRun run = step.StepRun; if (run != null) return run; if (step.Parent is ITestStep parent) { // recursively resolve the parent step's run. var parentRun = ResolveStepRun(parent, waiterStep); if (parentRun != null) { // if parentRun.StepThread is the same as the current thread it does not make sense to wait. return parentRun.WaitForChildStepStart(step.Id, parentRun.StepThread != TapThread.Current, waiterStep); } } return null; } internal static object GetOutput(IMemberData outputMember, object outputObject, Guid inputStepGuid) { var avail = outputMember.GetAttribute()?.Availability ?? OutputAttribute.DefaultAvailability; if (avail != OutputAvailability.BeforeRun && outputObject is ITestStep step ) { TestStepRun run = ResolveStepRun(step, inputStepGuid); if (run != null) run.WaitForOutput(avail, step); } return outputMember.GetValue(outputObject); } /// Updates the input of 'target' by reading the value of the source output. public static void UpdateInputs(ITestStepParent target) { checkRelations(target); var outputs = getOutputRelations(target); if (outputs.Length == 0) return; Action defer = () => { }; foreach (var connection in outputs) { if (connection.OutputObject is ITestStepParent src) { var outputMember = connection.OutputMember; var inputMember = connection.InputMember; if (outputMember == null || inputMember == null) { defer = defer.Bind(Unassign, connection); continue; } object value = GetOutput(connection.OutputMember, src, connection.InputObject is ITestStep step ? step.Id : Guid.NewGuid()); try { value = ConvertValue(value, connection.InputMember.TypeDescriptor); } catch (Exception convertException) { throw new Exception($"Unable to convert value for the '{connection.inputMember.Name}' setting: {convertException.Message}", convertException); } value = AssignOutputEvent.Invoke(target, value, connection.InputMember); connection.InputMember.SetValue(target, value); } } defer(); } internal static bool CanConvert(ITypeData to, ITypeData from) { if (from.DescendsTo(to)) return true; if (to is TypeData to2 && from is TypeData from2) { if (from2.Type.IsEnum) { // if from is an enum, it has TypeCode Int, but we dont allow assigning an enum to a int input. if (to2.IsString) return true; return false; } if (to2.IsNumeric || to2.IsString || to2.Type == typeof(bool)) { switch (from2.TypeCode) { case TypeCode.Double: case TypeCode.Single: case TypeCode.Int32: case TypeCode.Int16: case TypeCode.Int64: case TypeCode.UInt32: case TypeCode.UInt16: case TypeCode.UInt64: case TypeCode.Byte: case TypeCode.SByte: case TypeCode.Decimal: case TypeCode.Boolean: return true; case TypeCode.String: return to2.IsString; default: return false; } } } return false; } internal static object ConvertValue(object value, ITypeData to) { if (value == null) return null; if (to is TypeData td1 && td1.TypeCode != TypeCode.Object) { if(td1.Type != value.GetType()) return Convert.ChangeType(value, td1.Type); } return value; } static readonly AcceleratedDynamicMember Inputs = new AcceleratedDynamicMember() { ValueSetter = (owner, value) => ((IInputOutputRelations) owner).Inputs = (InputOutputRelation[])value, ValueGetter = (owner) => ((IInputOutputRelations) owner).Inputs, Name = "ConnectedInputs", DefaultValue = Array.Empty(), DeclaringType = TypeData.FromType(typeof(ITestStep)), Writable = true, Readable = true, TypeDescriptor = TypeData.FromType(typeof(InputOutputRelation[])) }; static readonly AcceleratedDynamicMember Outputs = new AcceleratedDynamicMember() { ValueSetter = (owner, value) => ((IInputOutputRelations) owner).Outputs = (InputOutputRelation[])value, ValueGetter = (owner) => ((IInputOutputRelations) owner).Outputs, Name = "ConnectedOutputs", DefaultValue = Array.Empty(), DeclaringType = TypeData.FromType(typeof(ITestStep)), Writable = true, Readable = true, TypeDescriptor = TypeData.FromType(typeof(InputOutputRelation[])) }; static InputOutputRelation[] getOutputRelations(ITestStepParent step) => (InputOutputRelation[]) Inputs.GetValue(step) ?? Array.Empty(); /// Sets all the output relations from an object. static void SetOutputRelations(ITestStepParent step, InputOutputRelation[] specs) => Inputs.SetValue(step, specs ?? Array.Empty()); static InputOutputRelation[] getInputRelations(ITestStepParent step) => (InputOutputRelation[]) Outputs.GetValue(step) ?? Array.Empty(); /// Gets a list of all the input relations to an object. static InputOutputRelation[] GetInputRelations(ITestStepParent step) { if (Outputs.GetValue(step) == null) return Array.Empty(); checkRelations(step); return getInputRelations(step); } /// Gets a list of all the output relations from an object. static InputOutputRelation[] GetOutputRelations(ITestStepParent step) { if (Inputs.GetValue(step) == null) return Array.Empty(); checkRelations(step); return getOutputRelations(step); } /// Get input/output relations to/from a test step. public static IEnumerable GetRelations(ITestStepParent step) { if (Inputs.GetValue(step) == null && Outputs.GetValue(step) == null) return Array.Empty(); checkRelations(step); return getOutputRelations(step).Concat(getInputRelations(step)); } /// Sets a list of all the input relations to an object. static void SetInputRelations(ITestStepParent step, InputOutputRelation[] specs) => Outputs.SetValue(step, specs ?? Array.Empty()); } }