using System.ComponentModel; using Artemis.Core.Tests.Infrastructure.Plugins.Modules; using Artemis.Core.Tests.Infrastructure.VisualScripting; namespace Artemis.Core.Tests.VisualScripting; public class PinTests { private readonly TestNode _source = new("Source"); private readonly TestNode _target = new("Target"); #region Connecting [Fact] public void ConnectTo_ConnectsBothPins() { OutputPin output = _source.CreateOutputPin(); InputPin input = _target.CreateInputPin(); input.ConnectTo(output); input.ConnectedTo.ShouldBe([output]); output.ConnectedTo.ShouldBe([input]); } [Fact] public void ConnectTo_RaisesPinConnectedOnBothPins() { OutputPin output = _source.CreateOutputPin(); InputPin input = _target.CreateInputPin(); List inputConnected = []; List outputConnected = []; input.PinConnected += (_, e) => inputConnected.Add(e.Value); output.PinConnected += (_, e) => outputConnected.Add(e.Value); input.ConnectTo(output); inputConnected.ShouldBe([output]); outputConnected.ShouldBe([input]); } [Fact] public void ConnectTo_RaisesPropertyChangedForConnectedTo() { OutputPin output = _source.CreateOutputPin(); InputPin input = _target.CreateInputPin(); List changed = []; input.PropertyChanged += (_, e) => changed.Add(e.PropertyName); input.ConnectTo(output); changed.ShouldContain(nameof(IPin.ConnectedTo)); } [Fact] public void Output_CanFeedSeveralInputs() { OutputPin output = _source.CreateOutputPin(); InputPin first = _target.CreateInputPin(); InputPin second = _target.CreateInputPin(); _source.OnEvaluate = _ => output.Value = new Numeric(3); first.ConnectTo(output); second.ConnectTo(output); output.ConnectedTo.ShouldBe([first, second]); _target.Reset(); first.Value.ShouldBe(new Numeric(3)); second.Value.ShouldBe(new Numeric(3)); } [Fact] public void ConnectTo_DoesNotCheckTypesOrDirections() { // The editor decides what may be connected, in code anything goes InputPin text = _source.CreateInputPin(); InputPin number = _target.CreateInputPin(); text.ConnectTo(number); text.ConnectedTo.ShouldBe([number]); number.ConnectedTo.ShouldBe([text]); } [Fact] public void ConnectTo_DoesNotLimitInputsToOneConnection() { // The editor disconnects an input before connecting it again (see ConnectPins in Artemis.UI.Shared). An input // with several connections reads from the first. OutputPin first = _source.CreateOutputPin(); OutputPin second = _source.CreateOutputPin(); InputPin input = _target.CreateInputPin(); _source.OnEvaluate = _ => { first.Value = new Numeric(1); second.Value = new Numeric(2); }; input.ConnectTo(first); input.ConnectTo(second); input.ConnectedTo.ShouldBe([first, second]); _target.Reset(); input.Value.ShouldBe(new Numeric(1)); } [Fact] public void ConnectTo_TheSamePinTwice_ConnectsThemOnce() { OutputPin output = _source.CreateOutputPin(); InputPin input = _target.CreateInputPin(); input.ConnectTo(output); input.ConnectTo(output); input.ConnectedTo.ShouldBe([output]); output.ConnectedTo.ShouldBe([input]); input.DisconnectFrom(output); input.ConnectedTo.ShouldBeEmpty(); output.ConnectedTo.ShouldBeEmpty(); } [Fact] public void DisconnectFrom_DisconnectsBothPins_AndRaisesPinDisconnectedOnBoth() { OutputPin output = _source.CreateOutputPin(); InputPin input = _target.CreateInputPin(); input.ConnectTo(output); List inputDisconnected = []; List outputDisconnected = []; input.PinDisconnected += (_, e) => inputDisconnected.Add(e.Value); output.PinDisconnected += (_, e) => outputDisconnected.Add(e.Value); input.DisconnectFrom(output); input.ConnectedTo.ShouldBeEmpty(); output.ConnectedTo.ShouldBeEmpty(); inputDisconnected.ShouldBe([output]); outputDisconnected.ShouldBe([input]); } [Fact] public void DisconnectAll_DisconnectsEveryConnection() { OutputPin output = _source.CreateOutputPin(); InputPin first = _target.CreateInputPin(); InputPin second = _target.CreateInputPin(); output.ConnectTo(first); output.ConnectTo(second); List outputDisconnected = []; int firstDisconnected = 0; output.PinDisconnected += (_, e) => outputDisconnected.Add(e.Value); first.PinDisconnected += (_, _) => firstDisconnected++; output.DisconnectAll(); output.ConnectedTo.ShouldBeEmpty(); first.ConnectedTo.ShouldBeEmpty(); second.ConnectedTo.ShouldBeEmpty(); outputDisconnected.ShouldBe([first, second]); firstDisconnected.ShouldBe(1); } [Fact] public void DisconnectAll_OnAPinWithoutConnections_RaisesNothing() { InputPin input = _target.CreateInputPin(); int raised = 0; input.PinDisconnected += (_, _) => raised++; input.PropertyChanged += (_, _) => raised++; input.DisconnectAll(); raised.ShouldBe(0); } #endregion #region Type compatibility [Theory] [InlineData(typeof(Numeric), typeof(Numeric), true)] [InlineData(typeof(Numeric), typeof(float), false)] [InlineData(typeof(Numeric), typeof(int), false)] [InlineData(typeof(object), typeof(string), true)] [InlineData(typeof(object), typeof(Numeric), true)] [InlineData(typeof(string), typeof(object), false)] [InlineData(typeof(Enum), typeof(TestEnum), true)] [InlineData(typeof(TestEnum), typeof(Enum), false)] [InlineData(typeof(TestEnum), typeof(DayOfWeek), false)] public void InputPin_AcceptsValuesThatFitItsType(Type pinType, Type valueType, bool compatible) { InputPin pin = _target.CreateInputPin(pinType); pin.IsTypeCompatible(valueType).ShouldBe(compatible); } [Theory] [InlineData(typeof(Numeric), typeof(Numeric), true)] [InlineData(typeof(Numeric), typeof(float), false)] [InlineData(typeof(string), typeof(object), true)] [InlineData(typeof(object), typeof(string), false)] [InlineData(typeof(TestEnum), typeof(Enum), true)] [InlineData(typeof(Enum), typeof(TestEnum), false)] [InlineData(typeof(TestEnum), typeof(DayOfWeek), false)] public void OutputPin_FitsInputsThatTakeItsType(Type pinType, Type inputType, bool compatible) { OutputPin pin = _source.CreateOutputPin(pinType); pin.IsTypeCompatible(inputType).ShouldBe(compatible); } [Fact] public void IsTypeCompatible_OnlyMatchesASpecificEnumToAnyEnumWhenForgiving() { InputPin input = _target.CreateInputPin(typeof(Enum)); OutputPin output = _source.CreateOutputPin(typeof(TestEnum)); input.IsTypeCompatible(typeof(TestEnum), false).ShouldBeFalse(); output.IsTypeCompatible(typeof(Enum), false).ShouldBeFalse(); input.IsTypeCompatible(typeof(TestEnum), true).ShouldBeTrue(); output.IsTypeCompatible(typeof(Enum), true).ShouldBeTrue(); } [Fact] public void Pins_KnowWhetherTheyAreNumeric() { _target.CreateInputPin().IsNumeric.ShouldBeTrue(); _target.CreateInputPin(typeof(Numeric)).IsNumeric.ShouldBeTrue(); _source.CreateOutputPin().IsNumeric.ShouldBeTrue(); _source.CreateOutputPin(typeof(Numeric)).IsNumeric.ShouldBeTrue(); _target.CreateInputPin().IsNumeric.ShouldBeFalse(); _source.CreateOutputPin(typeof(int)).IsNumeric.ShouldBeFalse(); } #endregion #region Values [Fact] public void InputPin_WithoutConnection_HasTheDefaultValue() { _target.CreateInputPin().Value.ShouldBe(new Numeric(0)); _target.CreateInputPin().Value.ShouldBeNull(); _target.CreateInputPin(typeof(Numeric)).Value.ShouldBe(new Numeric(0)); _target.CreateInputPin(typeof(bool)).Value.ShouldBe(false); _target.CreateInputPin(typeof(string)).Value.ShouldBeNull(); } [Fact] public void ReadingAnInput_EvaluatesTheNodeOfTheConnectedOutput() { OutputPin output = _source.CreateOutputPin(); InputPin input = _target.CreateInputPin(); input.ConnectTo(output); _source.OnEvaluate = _ => output.Value = new Numeric(5); _target.Reset(); input.Value.ShouldBe(new Numeric(5)); _source.Evaluations.ShouldBe(1); } [Fact] public void Values_AreKeptUntilThePinsAreReset() { OutputPin output = _source.CreateOutputPin(); InputPin input = _target.CreateInputPin(); input.ConnectTo(output); float value = 1; _source.OnEvaluate = _ => output.Value = new Numeric(value); _target.Reset(); input.Value.ShouldBe(new Numeric(1)); value = 2; input.Value.ShouldBe(new Numeric(1)); _source.Evaluations.ShouldBe(1); _source.Reset(); _target.Reset(); input.Value.ShouldBe(new Numeric(2)); _source.Evaluations.ShouldBe(2); } [Fact] public void Reset_MarksThePinAsNotEvaluated() { OutputPin output = _source.CreateOutputPin(); output.Value = new Numeric(1); output.IsEvaluated.ShouldBeTrue(); output.Reset(); output.IsEvaluated.ShouldBeFalse(); } [Fact] public void SettingAValue_RaisesPropertyChangedForPinValue() { OutputPin output = _source.CreateOutputPin(); List changed = []; output.PropertyChanged += (_, e) => changed.Add(e.PropertyName); output.Value = new Numeric(1); changed.ShouldContain(nameof(IPin.PinValue)); output.PinValue.ShouldBe(new Numeric(1)); } [Fact] public void TypedInput_DoesNotConvertOtherNumberTypes() { // Numbers only flow between pins as Numeric, a Numeric input connected to a float output reads the default value OutputPin output = _source.CreateOutputPin(); InputPin input = _target.CreateInputPin(); input.ConnectTo(output); output.Value = 5f; input.Value.ShouldBe(new Numeric(0)); } [Fact] public void TypedInput_ReadsAMatchingValueFromAnObjectOutput() { OutputPin output = _source.CreateOutputPin(typeof(object)); InputPin input = _target.CreateInputPin(); input.ConnectTo(output); output.Value = new Numeric(5); _target.Reset(); input.Value.ShouldBe(new Numeric(5)); _target.Reset(); output.Value = 5; input.Value.ShouldBe(new Numeric(0)); } [Fact] public void ObjectInput_ReadsAnyValue() { OutputPin output = _source.CreateOutputPin(); InputPin input = _target.CreateInputPin(typeof(object)); input.ConnectTo(output); output.Value = new Numeric(5); input.Value.ShouldBe(new Numeric(5)); } [Fact] public void UntypedInput_ReadingAValueOfTheWrongType_Throws() { OutputPin output = _source.CreateOutputPin(); InputPin input = _target.CreateInputPin(typeof(Numeric)); input.ConnectTo(output); output.Value = 5f; Should.Throw(() => input.Value); } [Fact] public void UntypedInput_ForAValueType_TurnsNullIntoTheDefaultValue() { OutputPin output = _source.CreateOutputPin(typeof(object)); InputPin input = _target.CreateInputPin(typeof(int)); input.ConnectTo(output); output.Value = null; input.Value.ShouldBe(0); } [Fact] public void UntypedInput_ForAReferenceType_PassesNullOn() { OutputPin output = _source.CreateOutputPin(typeof(string)); InputPin input = _target.CreateInputPin(typeof(string)); input.ConnectTo(output); output.Value = null; input.Value.ShouldBeNull(); } [Fact] public void UntypedOutput_RejectsValuesOfTheWrongType() { OutputPin output = _source.CreateOutputPin(typeof(Numeric)); Should.Throw(() => output.Value = 5f); } [Fact] public void UntypedOutput_ForAValueType_TurnsNullIntoTheDefaultValue() { OutputPin output = _source.CreateOutputPin(typeof(Numeric)); output.Value = new Numeric(3); output.Value = null; output.Value.ShouldBe(new Numeric(0)); } [Fact] public void NewTypedInput_ReadsItsConnection_LikeAnUntypedInput() { OutputPin output = _source.CreateOutputPin(); InputPin typed = _target.CreateInputPin(); InputPin untyped = _target.CreateInputPin(typeof(Numeric)); typed.ConnectTo(output); untyped.ConnectTo(output); output.Value = new Numeric(5); typed.IsEvaluated.ShouldBeFalse(); typed.Value.ShouldBe(new Numeric(5)); untyped.Value.ShouldBe(new Numeric(5)); } [Fact] public void UntypedOutput_StartsWithTheDefaultValueOfItsType() { _source.CreateOutputPin(typeof(Numeric)).Value.ShouldBe(new Numeric(0)); _source.CreateOutputPin(typeof(string)).Value.ShouldBeNull(); } #endregion #region Changing types [Fact] public void InputPin_ChangeType_KeepsCompatibleConnectionsOnly() { OutputPin number = _source.CreateOutputPin(); OutputPin text = _source.CreateOutputPin(); InputPin first = _target.CreateInputPin(typeof(object)); InputPin second = _target.CreateInputPin(typeof(object)); first.ConnectTo(number); second.ConnectTo(text); first.ChangeType(typeof(Numeric)); second.ChangeType(typeof(Numeric)); first.Type.ShouldBe(typeof(Numeric)); first.IsNumeric.ShouldBeTrue(); first.ConnectedTo.ShouldBe([number]); second.ConnectedTo.ShouldBeEmpty(); text.ConnectedTo.ShouldBeEmpty(); } [Fact] public void InputPin_ChangeType_RaisesPropertyChangedForType() { InputPin input = _target.CreateInputPin(typeof(object)); List changed = []; input.PropertyChanged += (_, e) => changed.Add(e.PropertyName); input.ChangeType(typeof(Numeric)); changed.ShouldContain(nameof(IPin.Type)); changed.ShouldContain(nameof(IPin.IsNumeric)); } [Fact] public void ChangeType_ToTheSameType_LeavesConnectionsAlone() { OutputPin output = _source.CreateOutputPin(typeof(Numeric)); InputPin input = _target.CreateInputPin(); input.ConnectTo(output); int disconnects = 0; output.PinDisconnected += (_, _) => disconnects++; output.ChangeType(typeof(Numeric)); disconnects.ShouldBe(0); output.ConnectedTo.ShouldBe([input]); } [Fact] public void OutputPin_ChangeType_KeepsCompatibleConnectionsOnly_AndResetsItsValue() { OutputPin output = _source.CreateOutputPin(typeof(object)); InputPin objectInput = _target.CreateInputPin(); InputPin numericInput = _target.CreateInputPin(); output.ConnectTo(objectInput); output.ConnectTo(numericInput); output.Value = "Hello"; output.ChangeType(typeof(string)); output.Type.ShouldBe(typeof(string)); output.ConnectedTo.ShouldBe([objectInput]); numericInput.ConnectedTo.ShouldBeEmpty(); output.Value.ShouldBeNull(); } [Fact] public void OutputPin_ChangeType_ToAValueType_UsesItsDefaultValue() { OutputPin output = _source.CreateOutputPin(typeof(object)); output.ChangeType(typeof(Numeric)); output.Value.ShouldBe(new Numeric(0)); output.IsNumeric.ShouldBeTrue(); } #endregion [Fact] public void Pin_KnowsItsNodeNameAndDirection() { InputPin input = _target.CreateInputPin("Amount"); OutputPin output = _source.CreateOutputPin(typeof(string), "Text"); input.Node.ShouldBe(_target); input.Name.ShouldBe("Amount"); input.Direction.ShouldBe(PinDirection.Input); input.Type.ShouldBe(typeof(Numeric)); output.Node.ShouldBe(_source); output.Name.ShouldBe("Text"); output.Direction.ShouldBe(PinDirection.Output); output.Type.ShouldBe(typeof(string)); } [Fact] public void Pin_RaisesPropertyChangedWhenRenamed() { InputPin input = _target.CreateInputPin("Amount"); PropertyChangedEventArgs? args = null; input.PropertyChanged += (_, e) => args = e; input.Name = "Total"; args.ShouldNotBeNull().PropertyName.ShouldBe(nameof(IPin.Name)); } }