From 57ddd5e0bf82b8b2add6d870a50899fce80abaeb Mon Sep 17 00:00:00 2001 From: helto4real Date: Sun, 17 May 2020 10:58:32 +0200 Subject: [PATCH] Tests of Rx --- .../Common/Reactive/AppDaemonRxApp.cs | 81 ++++--- .../NetDaemon.Daemon/Daemon/NetDaemonHost.cs | 5 +- .../DaemonHostTestBase.cs | 29 ++- .../Demon/NetDaemonHostTests.cs | 6 +- .../NetDaemon.Daemon.Tests/HassClientMock.cs | 206 ++++++++-------- .../NetDaemonApp/RxAppTests.cs | 38 --- .../Reactive/RxAppTest.cs | 224 ++++++++++++++++++ .../Reactive/RxSchedulers.cs | 51 ++++ 8 files changed, 463 insertions(+), 177 deletions(-) delete mode 100644 tests/NetDaemon.Daemon.Tests/NetDaemonApp/RxAppTests.cs create mode 100644 tests/NetDaemon.Daemon.Tests/Reactive/RxAppTest.cs create mode 100644 tests/NetDaemon.Daemon.Tests/Reactive/RxSchedulers.cs diff --git a/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs b/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs index ceb3f9669..b719594dd 100644 --- a/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs +++ b/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs @@ -5,9 +5,13 @@ using System.Linq; using System.Reactive.Concurrency; using System.Reactive.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +// For mocking +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] + namespace JoySoftware.HomeAssistant.NetDaemon.Common.Reactive { /// @@ -143,34 +147,7 @@ public IDisposable RunDaily(string time, Action action) /// public IDisposable RunEvery(TimeSpan timespan, Action action) { - var result = new DisposableTimerResult(_cancelTimers.Token); - - Observable.Interval(timespan, TaskPoolScheduler.Default) - .Subscribe( - s => - { - try - { - if (this.IsEnabled) - action(); - } - catch (OperationCanceledException) - { - // Do nothing - } - catch (Exception e) - { - LogError(e, "Error, RunEvery APP: {app}", Id ?? "unknown"); - } - }, - ex => - { - LogError(ex, "Error, RunEvery_ex APP: {app}", Id ?? "unknown"); - }, - () => Log("Exiting RunEvery for app {app}, {trigger}:{span}", Id!, timespan) - , result.Token); - - return result; + return CreateObservableIntervall(timespan, action); } /// @@ -279,7 +256,51 @@ public async override Task StartUpAsync(INetDaemon daemon) /// public EntityState? State(string entityId) => _daemon?.GetState(entityId); - private IDisposable CreateObservableTimer(DateTime timeOfDayToTrigger, TimeSpan interval, Action action) + /// + /// Creates an observable intervall + /// + /// Time span for intervall + /// The action to call + internal virtual IDisposable CreateObservableIntervall(TimeSpan timespan, Action action) + { + var result = new DisposableTimerResult(_cancelTimers.Token); + + Observable.Interval(timespan, TaskPoolScheduler.Default) + .Subscribe( + s => + { + try + { + if (this.IsEnabled) + action(); + } + catch (OperationCanceledException) + { + // Do nothing + } + catch (Exception e) + { + LogError(e, "Error, RunEvery APP: {app}", Id ?? "unknown"); + } + }, + ex => + { + LogError(ex, "Error, RunEvery_ex APP: {app}", Id ?? "unknown"); + }, + () => Log("Exiting RunEvery for app {app}, {trigger}:{span}", Id!, timespan) + , result.Token); + + return result; + } + + /// + /// Creates a observable timer that are tracked for errors + /// + /// When to start the timer + /// The intervall + /// Action to call each intervall + /// + internal virtual IDisposable CreateObservableTimer(DateTime timeOfDayToTrigger, TimeSpan interval, Action action) { var result = new DisposableTimerResult(_cancelTimers.Token); @@ -316,6 +337,4 @@ private IDisposable CreateObservableTimer(DateTime timeOfDayToTrigger, TimeSpan return result; } } - - } \ No newline at end of file diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs b/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs index 4fe9932fb..58fa4a43f 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs @@ -63,6 +63,7 @@ public class NetDaemonHost : INetDaemonHost, IAsyncDisposable private readonly IHttpHandler? _httpHandler; private readonly IDataRepository? _repository; + private readonly ConcurrentDictionary _runningAppInstances = new ConcurrentDictionary(); @@ -286,6 +287,7 @@ public async Task Initialize() await LoadAllApps().ConfigureAwait(false); EnableApplicationDiscoveryServiceAsync(); } + /// public IFluentInputSelect InputSelect(INetDaemonApp app, params string[] inputSelectParams) { @@ -316,7 +318,7 @@ public void ListenCompanionServiceCall(string service, Func acti } public void ListenServiceCall(string domain, string service, Func action) - => _daemonServiceCallFunctions.Add((domain.ToLowerInvariant(), service.ToLowerInvariant(), action)); + => _daemonServiceCallFunctions.Add((domain.ToLowerInvariant(), service.ToLowerInvariant(), action)); /// public IMediaPlayer MediaPlayer(INetDaemonApp app, params string[] entityIds) @@ -1250,6 +1252,7 @@ async Task SetStateOnDaemonAppSwitch(string state, dynamic? data) await SetStateAsync(entityId, state).ConfigureAwait(false); } } + private async Task RestoreAppState(INetDaemonAppBase appInstance) { try diff --git a/tests/NetDaemon.Daemon.Tests/DaemonHostTestBase.cs b/tests/NetDaemon.Daemon.Tests/DaemonHostTestBase.cs index 861b60141..52c746724 100644 --- a/tests/NetDaemon.Daemon.Tests/DaemonHostTestBase.cs +++ b/tests/NetDaemon.Daemon.Tests/DaemonHostTestBase.cs @@ -2,6 +2,7 @@ using JoySoftware.HomeAssistant.NetDaemon.Daemon.Storage; using Moq; using NetDaemon.Daemon.Tests.Daemon; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Dynamic; @@ -23,6 +24,7 @@ public partial class DaemonHostTestBase : IAsyncLifetime private readonly Mock _defaultDataRepositoryMock; private readonly HassClientMock _defaultHassClientMock; private readonly HttpHandlerMock _defaultHttpHandlerMock; + private readonly Mock _defaultMockedRxApp; private readonly LoggerMock _loggerMock; private readonly NetDaemonHost _notConnectedDaemonHost; @@ -42,28 +44,32 @@ internal DaemonHostTestBase() _defaultDaemonApp = new BaseTestApp(); _defaultDaemonApp.Id = "app_id"; + _defaultDaemonApp.IsEnabled = true; _defaultDaemonHost.InternalRunningAppInstances[_defaultDaemonApp.Id!] = _defaultDaemonApp; _defaultDaemonRxApp = new BaseTestRxApp(); _defaultDaemonRxApp.Id = "app_rx_id"; + _defaultDaemonRxApp.IsEnabled = true; _defaultDaemonHost.InternalRunningAppInstances[_defaultDaemonRxApp.Id!] = _defaultDaemonRxApp; + _defaultMockedRxApp = new Mock() { CallBase = true }; + _defaultMockedRxApp.Object.Id = "app_rx_mock_id"; + _defaultMockedRxApp.Object.IsEnabled = true; + _defaultMockedRxApp.Setup(n => n.CreateObservableIntervall(It.IsAny(), It.IsAny())).Returns(new Mock().Object); + _defaultDaemonHost.InternalRunningAppInstances[_defaultMockedRxApp.Object.Id!] = _defaultMockedRxApp.Object; + _notConnectedDaemonHost = new NetDaemonHost(new Mock().Object, HassClientMock.MockConnectFalse.Object, _defaultDataRepositoryMock.Object, _loggerMock.LoggerFactory); } public JoySoftware.HomeAssistant.NetDaemon.Common.NetDaemonApp DefaultDaemonApp => _defaultDaemonApp; public NetDaemonHost DefaultDaemonHost => _defaultDaemonHost; - public BaseTestRxApp DefaultDaemonRxApp => _defaultDaemonRxApp; - public Mock DefaultDataRepositoryMock => _defaultDataRepositoryMock; - public HassClientMock DefaultHassClientMock => _defaultHassClientMock; - public HttpHandlerMock DefaultHttpHandlerMock => _defaultHttpHandlerMock; - + public Mock DefaultMockedRxApp => _defaultMockedRxApp; public string HelloWorldData => "Hello world!"; public LoggerMock LoggerMock => _loggerMock; @@ -75,6 +81,7 @@ async Task IAsyncLifetime.DisposeAsync() await _defaultDaemonApp.DisposeAsync().ConfigureAwait(false); await _defaultDaemonRxApp.DisposeAsync().ConfigureAwait(false); await _defaultDaemonHost.DisposeAsync().ConfigureAwait(false); + await _defaultMockedRxApp.Object.DisposeAsync().ConfigureAwait(false); } public dynamic GetDynamicDataObject(string testData = "testdata") @@ -101,6 +108,7 @@ public async Task InitializeAsync() { await _defaultDaemonApp.StartUpAsync(_defaultDaemonHost).ConfigureAwait(false); await _defaultDaemonRxApp.StartUpAsync(_defaultDaemonHost).ConfigureAwait(false); + await _defaultMockedRxApp.Object.StartUpAsync(_defaultDaemonHost).ConfigureAwait(false); } public (Task, CancellationTokenSource) ReturnRunningDefauldDaemonHostTask(short milliSeconds = 100, bool overrideDebugNotCancel = false) @@ -146,6 +154,17 @@ public async Task WaitUntilCanceled(Task task) } } + protected async Task GetConnectedNetDaemonTask(short milliSeconds = 100, bool overrideDebugNotCancel = false) + { + var cancelSource = Debugger.IsAttached && !overrideDebugNotCancel + ? new CancellationTokenSource() + : new CancellationTokenSource(milliSeconds); + + var daemonTask = _defaultDaemonHost.Run("host", 8123, false, "token", cancelSource.Token); + await WaitForDefaultDaemonToConnect(DefaultDaemonHost, cancelSource.Token); + return daemonTask; + } + protected async Task WaitForDefaultDaemonToConnect(NetDaemonHost daemonHost, CancellationToken stoppingToken) { var nrOfTimesCheckForConnectedState = 0; diff --git a/tests/NetDaemon.Daemon.Tests/Demon/NetDaemonHostTests.cs b/tests/NetDaemon.Daemon.Tests/Demon/NetDaemonHostTests.cs index 6d421ab4c..c74c125d7 100644 --- a/tests/NetDaemon.Daemon.Tests/Demon/NetDaemonHostTests.cs +++ b/tests/NetDaemon.Daemon.Tests/Demon/NetDaemonHostTests.cs @@ -363,8 +363,8 @@ public async Task CallServiceEventShouldCallCorrectFunction() { // ARRANGE var dynObject = GetDynamicDataObject(HelloWorldData); + var daemonTask = await GetConnectedNetDaemonTask(); - DefaultHassClientMock.AddCallServiceEvent("custom_domain", "any_service", dynObject); var isCalled = false; string? message = ""; @@ -377,7 +377,9 @@ public async Task CallServiceEventShouldCallCorrectFunction() return Task.CompletedTask; }); - await RunDefauldDaemonUntilCanceled(); + DefaultHassClientMock.AddCallServiceEvent("custom_domain", "any_service", dynObject); + + await daemonTask; // ASSERT Assert.True(isCalled); diff --git a/tests/NetDaemon.Daemon.Tests/HassClientMock.cs b/tests/NetDaemon.Daemon.Tests/HassClientMock.cs index be579a733..ba54fdae3 100644 --- a/tests/NetDaemon.Daemon.Tests/HassClientMock.cs +++ b/tests/NetDaemon.Daemon.Tests/HassClientMock.cs @@ -13,18 +13,14 @@ namespace NetDaemon.Daemon.Tests { public class HassClientMock : Mock { - internal ConcurrentQueue FakeEvents = new ConcurrentQueue(); - internal ConcurrentDictionary FakeStates = new ConcurrentDictionary(); - internal HassAreas Areas = new HassAreas(); internal HassDevices Devices = new HassDevices(); internal HassEntities Entities = new HassEntities(); - + internal ConcurrentQueue FakeEvents = new ConcurrentQueue(); + internal ConcurrentDictionary FakeStates = new ConcurrentDictionary(); public HassClientMock() { - // diable warnings for this method #pragma warning disable 8619, 8620 - // Setup common mocks Setup(x => x.ConnectAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) @@ -91,75 +87,6 @@ public static HassClientMock MockConnectFalse } } - private void SetupDefaultStates() - { - FakeStates["light.correct_entity"] = new HassState - { - EntityId = "light.correct_entity", - Attributes = new Dictionary - { - ["test"] = 100 - }, - State = "on" - }; - - FakeStates["light.correct_entity2"] = new HassState - { - EntityId = "light.correct_entity2", - Attributes = new Dictionary - { - ["test"] = 101 - } - }; - - FakeStates["switch.correct_entity"] = new HassState - { - EntityId = "switch.correct_entity", - Attributes = new Dictionary - { - ["test"] = 105 - } - }; - - FakeStates["light.filtered_entity"] = new HassState - { - EntityId = "light.filtered_entity", - Attributes = new Dictionary - { - ["test"] = 90 - } - }; - FakeStates["binary_sensor.pir"] = new HassState - { - EntityId = "binary_sensor.pir", - State = "off", - Attributes = new Dictionary - { - ["device_class"] = "motion" - } - }; - - FakeStates["media_player.player"] = new HassState - { - EntityId = "media_player.player", - State = "off", - Attributes = new Dictionary - { - ["anyattribute"] = "some attribute" - } - }; - - FakeStates["light.ligth_in_area"] = new HassState - { - EntityId = "light.ligth_in_area", - State = "off", - Attributes = new Dictionary - { - ["anyattribute"] = "some attribute" - } - }; - } - public void AddCallServiceEvent(string domain, string service, dynamic data) { // Todo: Refactor to smth smarter @@ -247,6 +174,39 @@ public void AddCustomEvent(string eventType, dynamic? data) }); } + public void AssertEqual(HassState hassState, EntityState entity) + { + Assert.Equal(hassState.EntityId, entity.EntityId); + Assert.Equal(hassState.State, entity.State); + Assert.Equal(hassState.LastChanged, entity.LastChanged); + Assert.Equal(hassState.LastUpdated, entity.LastUpdated); + + if (hassState.Attributes?.Keys == null || entity.Attribute == null) + return; + + foreach (var attribute in hassState.Attributes!.Keys) + { + var attr = entity.Attribute as IDictionary ?? + throw new NullReferenceException($"{nameof(entity.Attribute)} catn be null"); + + Assert.True(attr.ContainsKey(attribute)); + Assert.Equal(hassState.Attributes[attribute], + attr![attribute]); + } + } + + /// + /// Gets a cancellation source that does not timeout if debugger is attached + /// + /// + /// + public CancellationTokenSource GetSourceWithTimeout(int milliSeconds = 100) + { + return (Debugger.IsAttached) + ? new CancellationTokenSource() + : new CancellationTokenSource(milliSeconds); + } + public void VerifyCallService(string domain, string service, params (string attribute, object value)[] attributesTuples) { @@ -277,37 +237,83 @@ public void VerifySetStateTimes(string entity, Times times) Verify(n => n.SetState(entity, It.IsAny(), It.IsAny()), times); } - public void AssertEqual(HassState hassState, EntityState entity) + private void SetupDefaultStates() { - Assert.Equal(hassState.EntityId, entity.EntityId); - Assert.Equal(hassState.State, entity.State); - Assert.Equal(hassState.LastChanged, entity.LastChanged); - Assert.Equal(hassState.LastUpdated, entity.LastUpdated); + FakeStates["light.correct_entity"] = new HassState + { + EntityId = "light.correct_entity", + Attributes = new Dictionary + { + ["test"] = 100 + }, + State = "on" + }; - if (hassState.Attributes?.Keys == null || entity.Attribute == null) - return; + FakeStates["light.correct_entity2"] = new HassState + { + EntityId = "light.correct_entity2", + Attributes = new Dictionary + { + ["test"] = 101 + } + }; - foreach (var attribute in hassState.Attributes!.Keys) + FakeStates["switch.correct_entity"] = new HassState { - var attr = entity.Attribute as IDictionary ?? - throw new NullReferenceException($"{nameof(entity.Attribute)} catn be null"); + EntityId = "switch.correct_entity", + Attributes = new Dictionary + { + ["test"] = 105 + } + }; - Assert.True(attr.ContainsKey(attribute)); - Assert.Equal(hassState.Attributes[attribute], - attr![attribute]); - } - } + FakeStates["light.filtered_entity"] = new HassState + { + EntityId = "light.filtered_entity", + Attributes = new Dictionary + { + ["test"] = 90 + } + }; + FakeStates["binary_sensor.pir"] = new HassState + { + EntityId = "binary_sensor.pir", + State = "off", + Attributes = new Dictionary + { + ["device_class"] = "motion" + } + }; - /// - /// Gets a cancellation source that does not timeout if debugger is attached - /// - /// - /// - public CancellationTokenSource GetSourceWithTimeout(int milliSeconds = 100) - { - return (Debugger.IsAttached) - ? new CancellationTokenSource() - : new CancellationTokenSource(milliSeconds); + FakeStates["binary_sensor.pir_2"] = new HassState + { + EntityId = "binary_sensor.pir_2", + State = "off", + Attributes = new Dictionary + { + ["device_class"] = "motion" + } + }; + + FakeStates["media_player.player"] = new HassState + { + EntityId = "media_player.player", + State = "off", + Attributes = new Dictionary + { + ["anyattribute"] = "some attribute" + } + }; + + FakeStates["light.ligth_in_area"] = new HassState + { + EntityId = "light.ligth_in_area", + State = "off", + Attributes = new Dictionary + { + ["anyattribute"] = "some attribute" + } + }; } } } \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/NetDaemonApp/RxAppTests.cs b/tests/NetDaemon.Daemon.Tests/NetDaemonApp/RxAppTests.cs deleted file mode 100644 index 8684e6f4e..000000000 --- a/tests/NetDaemon.Daemon.Tests/NetDaemonApp/RxAppTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Reactive.Linq; - -using Xunit; -using JoySoftware.HomeAssistant.NetDaemon.Common.Reactive; - -namespace NetDaemon.Daemon.Tests.NetDaemonApp -{ - public class RxAppTests : DaemonHostTestBase - { - [Fact] - public async Task TestReact() - { - bool isRun = false; - using var ctx = DefaultDaemonRxApp.StateChanges - .Where(t => t.New.EntityId == "binary_sensor.pir") - .NDSameStateFor(TimeSpan.FromMilliseconds(50)) - .Subscribe(e => - { - isRun = true; - }); - DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); - - await RunDefauldDaemonUntilCanceled(500); - - Assert.True(isRun); - } - } - - //class TestTraceListener : TraceListener - //{ - // ITestOutputHelper _output; - // public TestTraceListener(ITestOutputHelper output) { _output = output; } - // public override void Write(string message) { _output.WriteLine(message); } - // public override void WriteLine(string message) { _output.WriteLine(message); } - //} -} diff --git a/tests/NetDaemon.Daemon.Tests/Reactive/RxAppTest.cs b/tests/NetDaemon.Daemon.Tests/Reactive/RxAppTest.cs new file mode 100644 index 000000000..ac64920f2 --- /dev/null +++ b/tests/NetDaemon.Daemon.Tests/Reactive/RxAppTest.cs @@ -0,0 +1,224 @@ +using JoySoftware.HomeAssistant.NetDaemon.Common.Reactive; +using JoySoftware.HomeAssistant.NetDaemon.Daemon; +using System; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace NetDaemon.Daemon.Tests +{ + /// + /// Tests the fluent API parts of the daemon + /// + /// + /// Mainly the tests checks if correct underlying call to "CallService" + /// has been made. + /// + public class RxAppTest : DaemonHostTestBase + { + public RxAppTest() : base() + { + } + + [Fact] + public async Task NewAllEventDataShouldCallFunction() + { + // ARRANGE + var daemonTask = await GetConnectedNetDaemonTask(); + var called = false; + + // ACT + DefaultDaemonRxApp.StateAllChanges + .Subscribe(s => + { + called = true; + }); + + DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "on", "on"); + + await daemonTask; + + // ASSERT + Assert.True(called); + } + + [Fact] + public async Task NewEventShouldCallFunction() + { + // ARRANGE + var daemonTask = await GetConnectedNetDaemonTask(); + var called = false; + + // ACT + DefaultDaemonRxApp.StateChanges + .Subscribe(s => + { + called = true; + }); + + DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); + + await daemonTask; + + // ASSERT + Assert.True(called); + } + + [Fact] + public async Task SameStateEventShouldNotCallFunction() + { + // ARRANGE + var daemonTask = await GetConnectedNetDaemonTask(); + var called = false; + + // ACT + DefaultDaemonRxApp.StateChanges + .Subscribe(s => + { + called = true; + }); + + DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "on", "on"); + + await daemonTask; + + // ASSERT + Assert.False(called); + } + + [Fact] + public async Task StartupAsyncShouldThrowIfDaemonIsNull() + { + INetDaemonHost? host = null; + + // ARRANGE ACT ASSERT + await Assert.ThrowsAsync(() => DefaultDaemonRxApp.StartUpAsync(host!)); + } + + [Fact] + public async Task StateShouldReturnCorrectEntity() + { + // ARRANGE + var daemonTask = await GetConnectedNetDaemonTask(); + + // ACT + var entity = DefaultDaemonRxApp.State("binary_sensor.pir"); + + await daemonTask; + + // ASSERT + Assert.NotNull(entity); + } + [Fact] + public async Task UsingEntitiesLambdaNewEventShouldCallFunction() + { + // ARRANGE + var daemonTask = await GetConnectedNetDaemonTask(); + var called = false; + + // ACT + DefaultDaemonRxApp.Entities(n => n.EntityId.StartsWith("binary_sensor.pir")) + .StateChanges + .Subscribe(s => + { + called = true; + }); + + DefaultHassClientMock.AddChangedEvent("binary_sensor.pir_2", "off", "on"); + + await daemonTask; + + // ASSERT + Assert.True(called); + } + + [Fact] + public async Task UsingEntitiesNewEventShouldCallFunction() + { + // ARRANGE + var daemonTask = await GetConnectedNetDaemonTask(); + var called = false; + + // ACT + DefaultDaemonRxApp.Entities("binary_sensor.pir", "binary_sensor.pir_2") + .StateChanges + .Subscribe(s => + { + called = true; + }); + + DefaultHassClientMock.AddChangedEvent("binary_sensor.pir_2", "off", "on"); + + await daemonTask; + + // ASSERT + Assert.True(called); + } + + [Fact] + public async Task UsingEntityNewEventShouldCallFunction() + { + // ARRANGE + var daemonTask = await GetConnectedNetDaemonTask(); + var called = false; + + // ACT + DefaultDaemonRxApp.Entity("binary_sensor.pir") + .StateChanges + .Subscribe(s => + { + called = true; + }); + + DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); + + await daemonTask; + + // ASSERT + Assert.True(called); + } + + [Fact] + public async Task UsingEntityNewEventShouldNotCallFunction() + { + // ARRANGE + var daemonTask = await GetConnectedNetDaemonTask(); + var called = false; + + // ACT + DefaultDaemonRxApp.Entity("binary_sensor.other_pir") + .StateChanges + .Subscribe(s => + { + called = true; + }); + + DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); + + await daemonTask; + + // ASSERT + Assert.False(called); + } + [Fact] + public async Task WhenStateStaysSameForTimeItShouldCallFunction() + { + var daemonTask = await GetConnectedNetDaemonTask(300); + + bool isRun = false; + using var ctx = DefaultDaemonRxApp.StateChanges + .Where(t => t.New.EntityId == "binary_sensor.pir") + .NDSameStateFor(TimeSpan.FromMilliseconds(50)) + .Subscribe(e => + { + isRun = true; + }); + + DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); + + await daemonTask; + + Assert.True(isRun); + } + } +} \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/Reactive/RxSchedulers.cs b/tests/NetDaemon.Daemon.Tests/Reactive/RxSchedulers.cs new file mode 100644 index 000000000..37717dda6 --- /dev/null +++ b/tests/NetDaemon.Daemon.Tests/Reactive/RxSchedulers.cs @@ -0,0 +1,51 @@ +using Moq; +using System; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace NetDaemon.Daemon.Tests +{ + /// + /// Tests the fluent API parts of the daemon + /// + /// + /// Mainly the tests checks if correct underlying call to "CallService" + /// has been made. + /// + public class RxSchedulerTest : DaemonHostTestBase + { + public RxSchedulerTest() : base() + { + } + + [Fact] + public void RunEveryShouldCallCreateObservableIntervall() + { + // ARRANGE + + // ACT + DefaultMockedRxApp.Object.RunEvery(TimeSpan.FromSeconds(5), () => System.Console.WriteLine("Test")); + + // ASSERT + DefaultMockedRxApp.Verify(n => n.CreateObservableIntervall(TimeSpan.FromSeconds(5), It.IsAny()), Times.Once()); + } + + [Fact] + public async Task RunInShouldCallFunction() + { + // ARRANGE + var called = false; + + // ACT + DefaultDaemonRxApp.RunIn(TimeSpan.FromMilliseconds(100), () => called = true); + + // ASSERT + await Task.Delay(10); + Assert.False(called); + + await Task.Delay(100); + Assert.True(called); + } + } +} \ No newline at end of file