diff --git a/CodeCasa.sln b/CodeCasa.sln index 23836a0..17aa403 100644 --- a/CodeCasa.sln +++ b/CodeCasa.sln @@ -37,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeCasa.AutomationPipeline EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeCasa.Lights.NetDaemon.Scenes", "src\CodeCasa.Lights.NetDaemon.Scenes\CodeCasa.Lights.NetDaemon.Scenes.csproj", "{AA5FEE82-9A79-4AA7-BC3E-4769638FF8A6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeCasa.AutomationPipelines.Lights.Tests", "tests\CodeCasa.AutomationPipelines.Lights.Tests\CodeCasa.AutomationPipelines.Lights.Tests.csproj", "{96DB93C1-036A-436A-AF7A-AEC07243A929}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -107,6 +109,10 @@ Global {AA5FEE82-9A79-4AA7-BC3E-4769638FF8A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {AA5FEE82-9A79-4AA7-BC3E-4769638FF8A6}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA5FEE82-9A79-4AA7-BC3E-4769638FF8A6}.Release|Any CPU.Build.0 = Release|Any CPU + {96DB93C1-036A-436A-AF7A-AEC07243A929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96DB93C1-036A-436A-AF7A-AEC07243A929}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96DB93C1-036A-436A-AF7A-AEC07243A929}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96DB93C1-036A-436A-AF7A-AEC07243A929}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -116,6 +122,7 @@ Global {E2FA49AB-BFC2-4DDD-B743-F5AAB96F3FEC} = {5BCD08C3-3034-4D08-AC01-2AB6DFD67C33} {FA26C18B-24D0-4F3D-958C-A9BA61861C65} = {5BCD08C3-3034-4D08-AC01-2AB6DFD67C33} {5C7FB111-095B-F881-7268-4284370C8AAA} = {5BCD08C3-3034-4D08-AC01-2AB6DFD67C33} + {96DB93C1-036A-436A-AF7A-AEC07243A929} = {5BCD08C3-3034-4D08-AC01-2AB6DFD67C33} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5AAE8D3A-9457-4676-8F57-B2D78594CCC7} diff --git a/src/CodeCasa.AutomationPipelines.Lights/CodeCasa.AutomationPipelines.Lights.csproj b/src/CodeCasa.AutomationPipelines.Lights/CodeCasa.AutomationPipelines.Lights.csproj index eb95380..1e78424 100644 --- a/src/CodeCasa.AutomationPipelines.Lights/CodeCasa.AutomationPipelines.Lights.csproj +++ b/src/CodeCasa.AutomationPipelines.Lights/CodeCasa.AutomationPipelines.Lights.csproj @@ -17,6 +17,10 @@ cc_icon.png https://github.com/DevJasperNL/CodeCasa/releases + + + + diff --git a/src/CodeCasa.AutomationPipelines.Lights/Nodes/FactoryNode.cs b/src/CodeCasa.AutomationPipelines.Lights/Nodes/FactoryNode.cs index 267d579..ddefcb2 100644 --- a/src/CodeCasa.AutomationPipelines.Lights/Nodes/FactoryNode.cs +++ b/src/CodeCasa.AutomationPipelines.Lights/Nodes/FactoryNode.cs @@ -1,11 +1,11 @@ namespace CodeCasa.AutomationPipelines.Lights.Nodes; -internal class FactoryNode(Func lightTransitionFactory) +internal class FactoryNode(Func stateFactory) : PipelineNode { /// protected override void InputReceived(TState? input) { - Output = lightTransitionFactory(input); + Output = stateFactory(input); } } \ No newline at end of file diff --git a/src/CodeCasa.AutomationPipelines.Lights/Nodes/LightTransitionNode.cs b/src/CodeCasa.AutomationPipelines.Lights/Nodes/LightTransitionNode.cs index f4179fc..71492fe 100644 --- a/src/CodeCasa.AutomationPipelines.Lights/Nodes/LightTransitionNode.cs +++ b/src/CodeCasa.AutomationPipelines.Lights/Nodes/LightTransitionNode.cs @@ -13,13 +13,11 @@ namespace CodeCasa.AutomationPipelines.Lights.Nodes public abstract class LightTransitionNode(IScheduler scheduler) : IPipelineNode { private readonly Subject _newOutputSubject = new(); - private LightTransition? _input; private LightParameters? _inputLightDestinationParameters; private DateTime? _inputStartOfTransition; private DateTime? _inputEndOfTransition; private LightTransition? _output; private bool _passThroughNextInput; - private bool _passThrough; private IDisposable? _scheduledAction; /// @@ -33,29 +31,31 @@ public abstract class LightTransitionNode(IScheduler scheduler) : IPipelineNode< /// public LightTransition? Input { - get => _input; + get; set { _scheduledAction?.Dispose(); // Always cancel scheduled actions when the input changes. // We save additional information on the light transition that we can later use to continue the transition if it would be interrupted. InputLightSourceParameters = _inputLightDestinationParameters; - _input = value; + field = value; _inputLightDestinationParameters = value?.LightParameters; var transitionTime = value?.TransitionTime; _inputStartOfTransition = DateTime.UtcNow; - _inputEndOfTransition = transitionTime == null ? null : _inputStartOfTransition + transitionTime; + _inputEndOfTransition = _inputStartOfTransition + transitionTime; if (_passThroughNextInput) { PassThrough = true; return; } + if (PassThrough) { - SetOutputInternal(_input); + SetOutputInternal(field); return; } - InputReceived(_input); + + InputReceived(field); } } @@ -110,24 +110,25 @@ protected void ScheduleInterpolatedLightTransitionUsingInputTransitionTime(Light /// public bool PassThrough { - get => _passThrough; + get; set { // Always reset _passThroughNextInput when PassThrough is explicitly called. _passThroughNextInput = false; - if (_passThrough == value) + if (field == value) { return; } _scheduledAction?.Dispose(); // Always cancel scheduled actions when the pass through value changes. - _passThrough = value; - if (_passThrough) + field = value; + if (field) { _scheduledAction = scheduler.ScheduleInterpolatedLightTransition(InputLightSourceParameters, - _inputLightDestinationParameters, _inputStartOfTransition, _inputEndOfTransition, SetOutputInternal); + _inputLightDestinationParameters, _inputStartOfTransition, _inputEndOfTransition, + SetOutputInternal); } } } diff --git a/src/CodeCasa.AutomationPipelines/CodeCasa.AutomationPipelines.csproj b/src/CodeCasa.AutomationPipelines/CodeCasa.AutomationPipelines.csproj index 804a41c..085a7f0 100644 --- a/src/CodeCasa.AutomationPipelines/CodeCasa.AutomationPipelines.csproj +++ b/src/CodeCasa.AutomationPipelines/CodeCasa.AutomationPipelines.csproj @@ -40,9 +40,7 @@ - - <_Parameter1>CodeCasa.AutomationPipelines.Tests - + diff --git a/src/CodeCasa.NetDaemon.Extensions.Observables/CodeCasa.NetDaemon.Extensions.Observables.csproj b/src/CodeCasa.NetDaemon.Extensions.Observables/CodeCasa.NetDaemon.Extensions.Observables.csproj index fdd0968..f82839e 100644 --- a/src/CodeCasa.NetDaemon.Extensions.Observables/CodeCasa.NetDaemon.Extensions.Observables.csproj +++ b/src/CodeCasa.NetDaemon.Extensions.Observables/CodeCasa.NetDaemon.Extensions.Observables.csproj @@ -36,10 +36,10 @@ + - diff --git a/src/CodeCasa.NetDaemon.Notifications.InputSelect/CodeCasa.NetDaemon.Notifications.InputSelect.csproj b/src/CodeCasa.NetDaemon.Notifications.InputSelect/CodeCasa.NetDaemon.Notifications.InputSelect.csproj index d0e0013..05c10c7 100644 --- a/src/CodeCasa.NetDaemon.Notifications.InputSelect/CodeCasa.NetDaemon.Notifications.InputSelect.csproj +++ b/src/CodeCasa.NetDaemon.Notifications.InputSelect/CodeCasa.NetDaemon.Notifications.InputSelect.csproj @@ -36,11 +36,9 @@ - + - - <_Parameter1>NetDaemon.Notifications.InputSelect.Tests - + diff --git a/tests/CodeCasa.AutomationPipelines.Lights.Tests/CodeCasa.AutomationPipelines.Lights.Tests.csproj b/tests/CodeCasa.AutomationPipelines.Lights.Tests/CodeCasa.AutomationPipelines.Lights.Tests.csproj new file mode 100644 index 0000000..14cdcc3 --- /dev/null +++ b/tests/CodeCasa.AutomationPipelines.Lights.Tests/CodeCasa.AutomationPipelines.Lights.Tests.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + latest + enable + enable + + + + + + + + + + + + + + + diff --git a/tests/CodeCasa.AutomationPipelines.Lights.Tests/FactoryNodeTests.cs b/tests/CodeCasa.AutomationPipelines.Lights.Tests/FactoryNodeTests.cs new file mode 100644 index 0000000..4121faa --- /dev/null +++ b/tests/CodeCasa.AutomationPipelines.Lights.Tests/FactoryNodeTests.cs @@ -0,0 +1,135 @@ +namespace CodeCasa.AutomationPipelines.Lights.Tests; + +using AutomationPipelines; +using Nodes; + +[TestClass] +public sealed class FactoryNodeTests +{ + [TestMethod] + public void FactoryNode_TransformsInput() + { + // Arrange + const string inputValue = "Test"; + const string expectedOutput = "Test_Transformed"; + string? emittedOutput = null; + + var factoryNode = new FactoryNode(input => $"{input}_Transformed"); + factoryNode.OnNewOutput.Subscribe(o => emittedOutput = o); + + // Act + factoryNode.Input = inputValue; + + // Assert + Assert.AreEqual(expectedOutput, emittedOutput); + Assert.AreEqual(expectedOutput, factoryNode.Output); + } + + [TestMethod] + public void FactoryNode_HandlesNullInput() + { + // Arrange + string? emittedOutput = null; + var factoryNode = new FactoryNode(input => input ?? "NULL"); + factoryNode.OnNewOutput.Subscribe(o => emittedOutput = o); + + // Act + factoryNode.Input = null; + + // Assert + Assert.AreEqual("NULL", emittedOutput); + Assert.AreEqual("NULL", factoryNode.Output); + } + + [TestMethod] + public void FactoryNode_ProducesNullOutput() + { + // Arrange + var emittedOutput = "NotNull"; + var factoryNode = new FactoryNode(_ => null); + factoryNode.OnNewOutput.Subscribe(o => emittedOutput = o); + + // Act + factoryNode.Input = "Test"; + + // Assert + Assert.IsNull(emittedOutput); + Assert.IsNull(factoryNode.Output); + } + + [TestMethod] + public void FactoryNode_MultipleInputs() + { + // Arrange + var outputs = new List(); + var factoryNode = new FactoryNode(input => $"{input}_transformed"); + factoryNode.OnNewOutput.Subscribe(o => outputs.Add(o)); + + // Act + factoryNode.Input = "First"; + factoryNode.Input = "Second"; + factoryNode.Input = "Third"; + + // Assert + CollectionAssert.AreEqual( + new[] { "First_transformed", "Second_transformed", "Third_transformed" }, + outputs); + Assert.AreEqual("Third_transformed", factoryNode.Output); + } + + [TestMethod] + public void FactoryNode_WithIntegerTransformation() + { + // Arrange + var emittedOutputs = new List(); + var factoryNode = new FactoryNode(input => input * 2); + factoryNode.OnNewOutput.Subscribe(o => emittedOutputs.Add(o)); + + // Act + factoryNode.Input = 5; + factoryNode.Input = 10; + factoryNode.Input = 0; + + // Assert + CollectionAssert.AreEqual(new[] { 10, 20, 0 }, emittedOutputs); + Assert.AreEqual(0, factoryNode.Output); + } + + [TestMethod] + public void FactoryNode_OutputNotificationCalledForEachInput() + { + // Arrange + var outputNotificationCount = 0; + var factoryNode = new FactoryNode(input => input); + factoryNode.OnNewOutput.Subscribe(_ => outputNotificationCount++); + + // Act + factoryNode.Input = "Test1"; + factoryNode.Input = "Test2"; + factoryNode.Input = "Test3"; + + // Assert + Assert.AreEqual(3, outputNotificationCount); + } + + [TestMethod] + public void FactoryNode_ChainedWithOtherNodes() + { + // Arrange + string? finalOutput = null; + var pipeline = new Pipeline(); + pipeline.OnNewOutput.Subscribe(o => finalOutput = o); + + var factoryNode1 = new FactoryNode(input => $"{input}_1"); + var factoryNode2 = new FactoryNode(input => $"{input}_2"); + + // Act + pipeline.SetDefault("Test"); + pipeline.RegisterNode(factoryNode1); + pipeline.RegisterNode(factoryNode2); + + // Assert + Assert.AreEqual("Test_1_2", finalOutput); + Assert.AreEqual("Test_1_2", pipeline.Output); + } +} diff --git a/tests/CodeCasa.AutomationPipelines.Lights.Tests/MSTestSettings.cs b/tests/CodeCasa.AutomationPipelines.Lights.Tests/MSTestSettings.cs new file mode 100644 index 0000000..aaf278c --- /dev/null +++ b/tests/CodeCasa.AutomationPipelines.Lights.Tests/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]