diff --git a/.linting/roslynator.config b/.linting/roslynator.config new file mode 100644 index 000000000..edcfc08ca --- /dev/null +++ b/.linting/roslynator.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.linting/roslynator.ruleset b/.linting/roslynator.ruleset new file mode 100644 index 000000000..c4cf0dcbf --- /dev/null +++ b/.linting/roslynator.ruleset @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.vscode/daemon.code-snippets b/.vscode/daemon.code-snippets index 4918506db..f2c7e35cb 100644 --- a/.vscode/daemon.code-snippets +++ b/.vscode/daemon.code-snippets @@ -12,6 +12,14 @@ // "description": "Log output to console" // } { + "nullcheck": { + "scope": "csharp", + "prefix": "nullcheck", + "body": [ + "_ = $1 ??", + " throw new NetDaemonArgumentNullException(nameof($1));" + ] + }, "fact": { "scope": "csharp", "prefix": "fact", diff --git a/.vscode/settings.json b/.vscode/settings.json index ba38fd66b..0c3496fd9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,26 @@ "**/obj": true, "**/Properties": true, "**/TestResults": true - } + }, + "omnisharp.enableRoslynAnalyzers": true, + "cSpell.words": [ + "Expando", + "Finalizers", + "Hass", + "Usings", + "Xunit", + "dayofweek", + "entityid", + "hassio", + "idguid", + "mulitinstance", + "mydomain", + "myevent", + "mylight", + "myscript", + "noexist", + "noquotes", + "parentidguid", + "useridguid" + ] } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/AppRuntimeInfo.cs b/src/App/NetDaemon.App/Common/AppRuntimeInfo.cs index d6fb11f5d..018c6e47e 100644 --- a/src/App/NetDaemon.App/Common/AppRuntimeInfo.cs +++ b/src/App/NetDaemon.App/Common/AppRuntimeInfo.cs @@ -33,12 +33,8 @@ public sealed class AppRuntimeInfo /// in app switch /// [JsonPropertyName("app_attributes")] - public Dictionary AppAttributes { get; set; } = new(); - - + public Dictionary AppAttributes { get; } = new(); } - - } diff --git a/src/App/NetDaemon.App/Common/Attributes.cs b/src/App/NetDaemon.App/Common/Attributes.cs index 1fa7f4f4e..85de630bb 100644 --- a/src/App/NetDaemon.App/Common/Attributes.cs +++ b/src/App/NetDaemon.App/Common/Attributes.cs @@ -21,21 +21,23 @@ public sealed class DisableLogAttribute : System.Attribute { // See the attribute guidelines at // http://go.microsoft.com/fwlink/?LinkId=85236 - private SupressLogType[] _logTypesToSupress; + private readonly SupressLogType[]? _logTypesToSupress; /// /// Default constructor /// /// List of logtypes to supress - public DisableLogAttribute(params SupressLogType[] logTypes) - { - _logTypesToSupress = logTypes; - } + public DisableLogAttribute(params SupressLogType[] logTypes) => _logTypesToSupress = logTypes; /// /// Log types to supress /// - public IEnumerable LogTypesToSupress => _logTypesToSupress; + public IEnumerable? LogTypesToSupress => _logTypesToSupress; + + /// + /// Log tupes used + /// + public SupressLogType[]? LogTypes { get; } } /// @@ -50,16 +52,6 @@ public sealed class HomeAssistantServiceCallAttribute : System.Attribute { } [System.AttributeUsage(System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] public sealed class HomeAssistantStateChangedAttribute : System.Attribute { - private readonly bool _allChanges; - - private readonly object? _from; - - private readonly object? _to; - - // See the attribute guidelines at - // http://go.microsoft.com/fwlink/?LinkId=85236 - private string _entityId; - /// /// Default constructor /// @@ -69,30 +61,30 @@ public sealed class HomeAssistantStateChangedAttribute : System.Attribute /// Get all changes, ie also attribute changes public HomeAssistantStateChangedAttribute(string entityId, object? to = null, object? from = null, bool allChanges = false) { - _entityId = entityId; - _to = to; - _from = from; - _allChanges = allChanges; + EntityId = entityId; + To = to; + From = from; + AllChanges = allChanges; } /// /// Get all changes, even if only attribute changes /// - public bool AllChanges => _allChanges; + public bool AllChanges { get; } /// /// Unique id of the entity /// - public string EntityId => _entityId; + public string EntityId { get; } /// /// From state filter /// - public object? From => _from; + public object? From { get; } /// /// To state filter /// - public object? To => _to; + public object? To { get; } } } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/Configuration/HomeAssistantSettings.cs b/src/App/NetDaemon.App/Common/Configuration/HomeAssistantSettings.cs index 918ee304d..463415acc 100644 --- a/src/App/NetDaemon.App/Common/Configuration/HomeAssistantSettings.cs +++ b/src/App/NetDaemon.App/Common/Configuration/HomeAssistantSettings.cs @@ -16,9 +16,9 @@ public class HomeAssistantSettings /// /// Connect using ssl /// - public bool Ssl { get; set; } = false; + public bool Ssl { get; set; } /// - /// Token to authorize + /// Token to authorize /// public string Token { get; set; } = string.Empty; } diff --git a/src/App/NetDaemon.App/Common/Configuration/NetDaemonSettings.cs b/src/App/NetDaemon.App/Common/Configuration/NetDaemonSettings.cs index aa558a8b4..297851b50 100644 --- a/src/App/NetDaemon.App/Common/Configuration/NetDaemonSettings.cs +++ b/src/App/NetDaemon.App/Common/Configuration/NetDaemonSettings.cs @@ -1,5 +1,7 @@ using System; +using System.Globalization; using System.IO; +using NetDaemon.Common.Exceptions; namespace NetDaemon.Common.Configuration { @@ -27,21 +29,22 @@ public class NetDaemonSettings /// the apps to be in the file paths and tries to find /// all apps recursivly /// - public string? AppSource { get; set; } = null; + public string? AppSource { get; set; } /// - /// Returns the directory path of AppSource + /// Returns the directory path of AppSource /// public string GetAppSourceDirectory() { var source = AppSource?.Trim() ?? Directory.GetCurrentDirectory(); - if (source.EndsWith(".csproj") || source.EndsWith(".dll")) + if (source.EndsWith(".csproj", true, CultureInfo.InvariantCulture) + || source.EndsWith(".dll", true, CultureInfo.InvariantCulture)) { source = Path.GetDirectoryName(source); } - return source ?? throw new NullReferenceException("Source cannot be null!"); + return source ?? throw new NetDaemonNullReferenceException("Source cannot be null!"); } } } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/DelayResult.cs b/src/App/NetDaemon.App/Common/DelayResult.cs index fa92f28c6..46c4b3949 100644 --- a/src/App/NetDaemon.App/Common/DelayResult.cs +++ b/src/App/NetDaemon.App/Common/DelayResult.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Concurrent; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -13,7 +14,7 @@ public class DelayResult : IDelayResult { private readonly INetDaemonApp _daemonApp; private readonly TaskCompletionSource _delayTaskCompletionSource; - private bool _isCanceled = false; + private bool _isCanceled; /// /// Constructor @@ -51,7 +52,7 @@ public void Cancel() #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls + private bool disposedValue; // To detect redundant calls /// /// Disposes the object and cancel delay @@ -60,6 +61,8 @@ public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); + // Suppress finalization. + GC.SuppressFinalize(this); } /// diff --git a/src/App/NetDaemon.App/Common/Exceptions/Exceptions.cs b/src/App/NetDaemon.App/Common/Exceptions/Exceptions.cs new file mode 100644 index 000000000..efa63350d --- /dev/null +++ b/src/App/NetDaemon.App/Common/Exceptions/Exceptions.cs @@ -0,0 +1,66 @@ +using System; + +namespace NetDaemon.Common.Exceptions +{ + /// + public class NetDaemonNullReferenceException : NullReferenceException + { + /// + public NetDaemonNullReferenceException() + { + } + + /// + public NetDaemonNullReferenceException(string? message) : base(message) + { + } + + /// + public NetDaemonNullReferenceException(string? message, Exception? innerException) : base(message, innerException) + { + } + } + + /// + public class NetDaemonException : Exception + { + /// + public NetDaemonException() + { + } + + /// + public NetDaemonException(string? message) : base(message) + { + } + + /// + public NetDaemonException(string? message, Exception? innerException) : base(message, innerException) + { + } + } + + /// + public class NetDaemonArgumentNullException : ArgumentNullException + { + /// + public NetDaemonArgumentNullException() + { + } + + /// + public NetDaemonArgumentNullException(string? paramName) : base(paramName) + { + } + + /// + public NetDaemonArgumentNullException(string? message, Exception? innerException) : base(message, innerException) + { + } + + /// + public NetDaemonArgumentNullException(string? paramName, string? message) : base(paramName, message) + { + } + } +} \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/ExtensionMethods.cs b/src/App/NetDaemon.App/Common/ExtensionMethods.cs index 626bc432a..39dab6826 100644 --- a/src/App/NetDaemon.App/Common/ExtensionMethods.cs +++ b/src/App/NetDaemon.App/Common/ExtensionMethods.cs @@ -4,13 +4,13 @@ using System.Globalization; using System.Text; using NetDaemon.Common.Fluent; - +using System.Diagnostics.CodeAnalysis; namespace NetDaemon.Common { /// /// Useful extension methods used /// - public static class Extensions + public static class NetDaemonExtensions { /// /// Converts a valuepair to dynamic object @@ -20,30 +20,39 @@ public static dynamic ToDynamic(this (string name, object val)[] attributeNameVa { // Convert the tuple name/value pair to tuple that can be serialized dynamically var attributes = new FluentExpandoObject(true, true); - foreach (var (attribute, value) in attributeNameValuePair) + if (attributeNameValuePair is not null) { - ((IDictionary)attributes).Add(attribute, value); + foreach (var (attribute, value) in attributeNameValuePair) + { + if (value is not null) + { + // We only add non-null values since the FluentExpandoObject will + // return null on missing anyway + attributes.Add(attribute, value); + } + } } - dynamic result = attributes; - return result; + return (dynamic)attributes; } /// /// Converts a anoumous type to expando object /// /// - /// public static ExpandoObject? ToExpandoObject(this object obj) { // Null-check IDictionary expando = new ExpandoObject(); - foreach (PropertyDescriptor? property in TypeDescriptor.GetProperties(obj.GetType())) + if (obj is not null) { - if (property is not null) - expando.Add(property.Name, property.GetValue(obj)); + foreach (PropertyDescriptor? property in TypeDescriptor.GetProperties(obj.GetType())) + { + if (property is not null) + expando.Add(property.Name, property.GetValue(obj)); + } } return (ExpandoObject)expando; @@ -53,10 +62,14 @@ public static dynamic ToDynamic(this (string name, object val)[] attributeNameVa /// Converts any unicode string to a safe Home Assistant name /// /// The unicode string to convert + [SuppressMessage(category: "Microsoft.Globalization", checkId: "CA1308")] public static string ToSafeHomeAssistantEntityId(this string str) { + if (str is null) + return string.Empty; + string normalizedString = str.Normalize(NormalizationForm.FormD); - StringBuilder stringBuilder = new StringBuilder(str.Length); + StringBuilder stringBuilder = new(str.Length); foreach (char c in normalizedString) { diff --git a/src/App/NetDaemon.App/Common/ExternalEvents.cs b/src/App/NetDaemon.App/Common/ExternalEvents.cs index b7b34819c..73efea5d5 100644 --- a/src/App/NetDaemon.App/Common/ExternalEvents.cs +++ b/src/App/NetDaemon.App/Common/ExternalEvents.cs @@ -5,13 +5,11 @@ namespace NetDaemon.Common { - /// - /// Base class for all external events + /// Base class for all external events /// public class ExternalEventBase { - } /// @@ -19,10 +17,8 @@ public class ExternalEventBase /// public class AppsInformationEvent : ExternalEventBase { - } - /// /// Information about the application /// @@ -56,7 +52,6 @@ public class ApplicationInfo /// Last known error message /// public string? LastErrorMessage { get; set; } - } /// @@ -72,6 +67,5 @@ public class ConfigInfo /// Settings Home Assistant related /// public HomeAssistantSettings? HomeAssistantSettings { get; set; } - } } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/Fluent/Context.cs b/src/App/NetDaemon.App/Common/Fluent/Context.cs index df0d27a99..2d770ded7 100644 --- a/src/App/NetDaemon.App/Common/Fluent/Context.cs +++ b/src/App/NetDaemon.App/Common/Fluent/Context.cs @@ -1,7 +1,7 @@ namespace NetDaemon.Common.Fluent { /// - /// + /// Context /// public class Context { @@ -10,13 +10,12 @@ public class Context /// public string Id { get; set; } = ""; /// - /// + /// ParentId /// public string? ParentId { get; set; } /// /// The id of the user who is responsible for the connected item. /// public string? UserId { get; set; } - } } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/Fluent/EntityBase.cs b/src/App/NetDaemon.App/Common/Fluent/EntityBase.cs index 784e2183b..c7ae8ab72 100644 --- a/src/App/NetDaemon.App/Common/Fluent/EntityBase.cs +++ b/src/App/NetDaemon.App/Common/Fluent/EntityBase.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; +using NetDaemon.Common.Exceptions; namespace NetDaemon.Common.Fluent { @@ -11,26 +11,26 @@ namespace NetDaemon.Common.Fluent public class EntityBase //: EntityState { internal readonly ConcurrentQueue _actions = - new ConcurrentQueue(); + new(); internal FluentAction? _currentAction; - internal StateChangedInfo _currentState = new StateChangedInfo(); + internal StateChangedInfo _currentState = new(); /// /// The daemon used in the API /// - protected readonly INetDaemonApp App; + protected INetDaemonApp App { get; } /// /// The daemon used in the API /// - protected readonly INetDaemon Daemon; + protected INetDaemon Daemon { get; } /// /// The EntityIds used /// - protected readonly IEnumerable EntityIds; + protected IEnumerable EntityIds { get; } /// /// Constructor @@ -48,9 +48,12 @@ public EntityBase(IEnumerable entityIds, INetDaemon daemon, INetDaemonAp /// protected static string GetDomainFromEntity(string entity) { + if (string.IsNullOrEmpty(entity)) + throw new NetDaemonNullReferenceException(nameof(entity)); + var entityParts = entity.Split('.'); if (entityParts.Length != 2) - throw new ApplicationException($"entity_id is mal formatted {entity}"); + throw new NetDaemonException($"entity_id is mal formatted {entity}"); return entityParts[0]; } diff --git a/src/App/NetDaemon.App/Common/Fluent/EntityManager.cs b/src/App/NetDaemon.App/Common/Fluent/EntityManager.cs index 71eefacf0..a720f5b37 100644 --- a/src/App/NetDaemon.App/Common/Fluent/EntityManager.cs +++ b/src/App/NetDaemon.App/Common/Fluent/EntityManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -9,7 +10,7 @@ namespace NetDaemon.Common.Fluent /// /// Implements interface for managing entities in the fluent API /// - public class EntityManager : EntityBase, IEntity, IAction, + public sealed class EntityManager : EntityBase, IEntity, IAction, IStateEntity, IState, IStateAction, IScript { /// @@ -39,19 +40,18 @@ public IExecute Call(Func func) /// IDelayResult IDelayStateChange.DelayUntilStateChange(object? to, object? from, bool allChanges) { - return App.DelayUntilStateChange(this.EntityIds, to, from, allChanges); + return App.DelayUntilStateChange(EntityIds, to, from, allChanges); } /// - IDelayResult IDelayStateChange.DelayUntilStateChange(Func stateFunc) - { - return App.DelayUntilStateChange(this.EntityIds, stateFunc); - } + IDelayResult IDelayStateChange.DelayUntilStateChange(Func stateFunc) => App.DelayUntilStateChange(EntityIds, stateFunc); /// + [SuppressMessage("Microsoft.Design", "CA1031")] public void Execute() { foreach (var entityId in EntityIds) + { App.ListenState(entityId, async (entityIdInn, newState, oldState) => { try @@ -74,13 +74,13 @@ public void Execute() } else { - if (_currentState.To != null) - if (_currentState.To != newState?.State) - return; + if (_currentState.To != null && _currentState.To != newState?.State) + { + return; + } - if (_currentState.From != null) - if (_currentState.From != oldState?.State) - return; + if (_currentState.From != null && _currentState.From != oldState?.State) + return; // If we don´t accept all changes in the state change // and we do not have a state change so return @@ -104,7 +104,9 @@ public void Execute() "State same {newState} during period of {time}, executing action!", $"{newState?.State}", _currentState.ForTimeSpan); // The state has not changed during the time we waited if (_currentState.FuncToCall == null) + { await entityManager.ExecuteAsync(true).ConfigureAwait(false); + } else { try @@ -121,11 +123,10 @@ public void Execute() } else { - App.LogDebug( - "State same {newState} but different state changed: {currentLastChanged}, expected {newLastChanged}", - $"{newState?.State}", - currentState?.LastChanged!, - newState?.LastChanged!); + App.LogDebug("State same {newState} but different state changed: {currentLastChanged}, expected {newLastChanged}", + $"{newState?.State}", + currentState?.LastChanged ?? DateTime.MinValue, + newState?.LastChanged ?? DateTime.MinValue); } } else @@ -152,15 +153,23 @@ public void Execute() } catch (Exception e) { - App.LogError(e, - "Call function error in App {appId}, EntityId: {entityId}, From: {newState} To: {oldState}", - App.Id!, entityIdInn, $"{newState?.State}", $"{oldState?.State}"); + App.LogError( + e, + "Call function error in App {appId}, EntityId: {entityId}, From: {newState} To: {oldState}", + App.Id!, + entityIdInn, + $"{newState?.State}", + $"{oldState?.State}"); } } else if (_currentState.ScriptToCall != null) + { await Daemon.RunScript(App, _currentState.ScriptToCall).ExecuteAsync().ConfigureAwait(false); + } else + { await entityManager.ExecuteAsync(true).ConfigureAwait(false); + } } } catch (OperationCanceledException) @@ -172,6 +181,7 @@ public void Execute() App.LogWarning(e, "Unhandled error in ListenState in App {appId}", App.Id!); } }); + } //} } @@ -189,8 +199,8 @@ async Task IScript.ExecuteAsync() foreach (var scriptName in EntityIds) { var name = scriptName; - if (scriptName.Contains('.')) - name = scriptName[(scriptName.IndexOf('.') + 1)..]; + if (scriptName.Contains('.', StringComparison.InvariantCulture)) + name = scriptName[(scriptName.IndexOf('.', StringComparison.InvariantCulture) + 1)..]; var task = Daemon.CallServiceAsync("script", name); taskList.Add(task); } @@ -209,15 +219,18 @@ async Task IScript.ExecuteAsync() /// You want to keep the items when using this as part of an automation /// that are kept over time. Not keeping when just doing a command /// - /// public async Task ExecuteAsync(bool keepItems) { if (keepItems) + { foreach (var action in _actions) await HandleAction(action).ConfigureAwait(false); + } else + { while (_actions.TryDequeue(out var fluentAction)) await HandleAction(fluentAction).ConfigureAwait(false); + } } /// @@ -230,8 +243,10 @@ public IExecute RunScript(params string[] entityIds) /// IAction ISetState.SetState(dynamic state) { - _currentAction = new FluentAction(FluentActionType.SetState); - _currentAction.State = state; + _currentAction = new FluentAction(FluentActionType.SetState) + { + State = state + }; _actions.Enqueue(_currentAction); return this; } diff --git a/src/App/NetDaemon.App/Common/Fluent/Fluent.cs b/src/App/NetDaemon.App/Common/Fluent/Fluent.cs index 476a22a7a..7f17bc4c0 100644 --- a/src/App/NetDaemon.App/Common/Fluent/Fluent.cs +++ b/src/App/NetDaemon.App/Common/Fluent/Fluent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace NetDaemon.Common.Fluent @@ -42,6 +43,7 @@ public interface IDelayStateChange /// The state change to, or null if any state /// The state changed from or null if any state /// Get all changed, even only attribute changes + [SuppressMessage("Microsoft.Naming", "CA1716")] IDelayResult DelayUntilStateChange(object? to = null, object? from = null, bool allChanges = false); /// @@ -95,6 +97,8 @@ public interface IScript /// /// Represent state change actions /// + [SuppressMessage("Microsoft.Naming", "CA1716")] + public interface IState { /// @@ -150,6 +154,7 @@ public interface IStateAction : IExecute /// /// When state change /// + [SuppressMessage("Microsoft.Naming", "CA1716")] public interface IStateChanged { /// @@ -229,6 +234,7 @@ public interface ISpeak /// Generic interface for stop /// /// Return type of stop operation + [SuppressMessage("Microsoft.Naming", "CA1716")] public interface IStop { /// diff --git a/src/App/NetDaemon.App/Common/Fluent/FluentCamera.cs b/src/App/NetDaemon.App/Common/Fluent/FluentCamera.cs index 6fad012d7..53c612f32 100644 --- a/src/App/NetDaemon.App/Common/Fluent/FluentCamera.cs +++ b/src/App/NetDaemon.App/Common/Fluent/FluentCamera.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using NetDaemon.Common.Exceptions; namespace NetDaemon.Common.Fluent { @@ -31,6 +33,7 @@ public interface IEnableMotionDetection /// /// Generic interface for PlayStream /// + [SuppressMessage("Microsoft.Naming", "CA1711")] public interface IPlayStream { /// @@ -79,9 +82,9 @@ public interface ICamera : IExecuteAsync, IDisableMotionDetection /// /// Implements the fluent camera interface /// - public class CameraManager : EntityBase, ICamera, IExecuteAsync + public class CameraManager : EntityBase, ICamera { - private (string, dynamic?)? _serviceCall = null; + private (string, dynamic?)? _serviceCall; /// /// Constructor @@ -122,14 +125,14 @@ public IExecuteAsync PlayStream(string mediaPlayerId, string? format = null) } /// - public IExecuteAsync Record(string fileName, int? duration = null, int? lookback = null) + public IExecuteAsync Record(string fileName, int? seconds = null, int? lookback = null) { dynamic serviceData = new FluentExpandoObject(); serviceData.filename = fileName; - if (duration is not null) - serviceData.duration = duration; + if (seconds is not null) + serviceData.duration = seconds; if (lookback is not null) serviceData.lookback = lookback; @@ -165,7 +168,7 @@ public IExecuteAsync TurnOn() /// public Task ExecuteAsync() { - (string service, dynamic? data) = _serviceCall ?? throw new NullReferenceException($"{nameof(_serviceCall)} is Null!"); + (string service, dynamic? data) = _serviceCall ?? throw new NetDaemonNullReferenceException($"{nameof(_serviceCall)} is Null!"); return CallServiceOnAllEntities(service, data); } diff --git a/src/App/NetDaemon.App/Common/Fluent/FluentEvent.cs b/src/App/NetDaemon.App/Common/Fluent/FluentEvent.cs index 78072a2cc..2cb47cf1a 100644 --- a/src/App/NetDaemon.App/Common/Fluent/FluentEvent.cs +++ b/src/App/NetDaemon.App/Common/Fluent/FluentEvent.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using NetDaemon.Common.Exceptions; namespace NetDaemon.Common.Fluent { /// /// Handles events in fluent API /// + [SuppressMessage("Microsoft.Naming", "CA1716")] public interface IFluentEvent { /// @@ -21,10 +24,10 @@ public interface IFluentEvent /// public class FluentEventManager : IFluentEvent, IExecute { - private IEnumerable? _events; - private INetDaemonApp _daemon; + private readonly IEnumerable? _events; + private readonly INetDaemonApp _daemon; private Func? _functionToCall; - private Func? _funcSelector; + private readonly Func? _funcSelector; /// /// Constructor @@ -52,7 +55,7 @@ public FluentEventManager(Func func, INetDaemonApp da public IExecute Call(Func? func) { if (func == null) - throw new NullReferenceException("Call function is null listening to event"); + throw new NetDaemonNullReferenceException("Call function is null listening to event"); _functionToCall = func; return this; @@ -62,13 +65,17 @@ public IExecute Call(Func? func) public void Execute() { if (_events == null && _funcSelector == null) - throw new NullReferenceException($"Both {nameof(_events)} or {nameof(_events)} cant be null"); + throw new NetDaemonNullReferenceException($"Both {nameof(_events)} or {nameof(_events)} cant be null"); if (_events != null) + { foreach (var ev in _events) _daemon.ListenEvent(ev, _functionToCall!); + } else + { _daemon.ListenEvent(_funcSelector!, _functionToCall!); + } } } @@ -83,9 +90,8 @@ public class FluentEventProperty public string EventId { get; set; } = ""; /// - /// + /// Data of the event /// - /// - public dynamic? Data { get; set; } = null; + public dynamic? Data { get; set; } } } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/Fluent/FluentExpandoObject.cs b/src/App/NetDaemon.App/Common/Fluent/FluentExpandoObject.cs index a5a79d148..593ae1caf 100644 --- a/src/App/NetDaemon.App/Common/Fluent/FluentExpandoObject.cs +++ b/src/App/NetDaemon.App/Common/Fluent/FluentExpandoObject.cs @@ -1,10 +1,13 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Dynamic; +using System.Globalization; using System.Linq; using System.Text.Json; using JoySoftware.HomeAssistant.Client; +using NetDaemon.Common.Exceptions; namespace NetDaemon.Common.Fluent { @@ -15,6 +18,7 @@ namespace NetDaemon.Common.Fluent /// Thanks to @lukevendediger for original code and inspiration /// https://gist.github.com/lukevenediger/6327599 /// + [SuppressMessage("Microsoft.Naming", "CA1710")] public class FluentExpandoObject : DynamicObject, IDictionary { private readonly Dictionary _dict = new(); @@ -72,7 +76,6 @@ public bool Contains(KeyValuePair item) /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { - throw new NotImplementedException(); } /// @@ -129,18 +132,22 @@ public bool TryGetValue(string key, out object value) /// public override bool TrySetMember(SetMemberBinder binder, object? value) { + if (binder is null) + throw new NetDaemonNullReferenceException(nameof(binder)); + UpdateDictionary(binder.Name, value); - if (_daemonApp != null) - { - // It is supposed to persist, this is the only reason _daemon is present - _daemonApp.SaveAppState(); - } + // It is supposed to persist, this is the only reason _daemon is present + _daemonApp?.SaveAppState(); + return true; } /// public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object? value) { + if (indexes is null) + throw new NetDaemonNullReferenceException(nameof(indexes)); + if (!(indexes[0] is string)) return base.TrySetIndex(binder, indexes, value); if (indexes[0] is string key) UpdateDictionary(NormalizePropertyName(key), value); @@ -150,6 +157,9 @@ public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object /// public override bool TryGetMember(GetMemberBinder binder, out object? result) { + if (binder is null) + throw new NetDaemonNullReferenceException(nameof(binder)); + var key = NormalizePropertyName(binder.Name); if (_dict.ContainsKey(key)) @@ -173,6 +183,9 @@ public override bool TryGetMember(GetMemberBinder binder, out object? result) /// Existing properties are not overwritten. public dynamic Augment(FluentExpandoObject obj) { + if (obj is null) + throw new NetDaemonNullReferenceException(nameof(obj)); + obj._dict .Where(pair => !_dict.ContainsKey(NormalizePropertyName(pair.Key))) .ToList() @@ -186,6 +199,9 @@ public dynamic Augment(FluentExpandoObject obj) /// The object to copy from public dynamic CopyFrom(IDictionary obj) { + if (obj is null) + throw new NetDaemonNullReferenceException(nameof(obj)); + // Clear any items before copy Clear(); @@ -212,7 +228,6 @@ public dynamic CopyFrom(IDictionary obj) /// Combine two instances together to get a union. /// /// the object to combine - /// public dynamic Augment(ExpandoObject obj) { obj @@ -225,6 +240,9 @@ public dynamic Augment(ExpandoObject obj) /// public T ValueOrDefault(string propertyName, T defaultValue) { + if (propertyName is null) + throw new NetDaemonNullReferenceException(nameof(propertyName)); + propertyName = NormalizePropertyName(propertyName); return _dict.ContainsKey(propertyName) ? (T)_dict[propertyName] @@ -237,6 +255,9 @@ public T ValueOrDefault(string propertyName, T defaultValue) /// Respects the case sensitivity setting public bool HasProperty(string name) { + if (name is null) + throw new NetDaemonNullReferenceException(nameof(name)); + return _dict.ContainsKey(NormalizePropertyName(name)); } @@ -250,17 +271,15 @@ public override string ToString() private void UpdateDictionary(string name, object? value) { - _ = value ?? throw new ArgumentNullException("value", "value cannot be null"); + _ = value ?? throw new ArgumentNullException(nameof(value), "value cannot be null"); var key = NormalizePropertyName(name); - if (_dict.ContainsKey(key)) - _dict[key] = value; - else - _dict.Add(key, value); + _dict[key] = value; } + [SuppressMessage("", "CA1308")] private string NormalizePropertyName(string propertyName) { - return _ignoreCase ? propertyName.ToLower() : propertyName; + return _ignoreCase ? propertyName.ToLower(CultureInfo.InvariantCulture) : propertyName; } } } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/Fluent/FluentInputSelect.cs b/src/App/NetDaemon.App/Common/Fluent/FluentInputSelect.cs index a9cb745ce..ed3ffecfe 100644 --- a/src/App/NetDaemon.App/Common/Fluent/FluentInputSelect.cs +++ b/src/App/NetDaemon.App/Common/Fluent/FluentInputSelect.cs @@ -30,9 +30,8 @@ public interface IFluentSetOption /// /// Set option value of the selected input selects /// - /// - /// - T SetOption(string option); + /// + T SetOption(string selectOption); } /// @@ -41,8 +40,8 @@ public interface IFluentSetOption public class InputSelectManager : IFluentInputSelect, IFluentExecuteAsync { private readonly IEnumerable _entityIds; - private INetDaemon _daemon; - private INetDaemonApp _app; + private readonly INetDaemon _daemon; + private readonly INetDaemonApp _app; private string? _option; /// @@ -75,9 +74,9 @@ public Task ExecuteAsync() } /// - public IFluentExecuteAsync SetOption(string option) + public IFluentExecuteAsync SetOption(string selectOption) { - _option = option; + _option = selectOption; return this; } } diff --git a/src/App/NetDaemon.App/Common/Fluent/FluentMediaPlayer.cs b/src/App/NetDaemon.App/Common/Fluent/FluentMediaPlayer.cs index 9bcb1251b..122ddc1cc 100644 --- a/src/App/NetDaemon.App/Common/Fluent/FluentMediaPlayer.cs +++ b/src/App/NetDaemon.App/Common/Fluent/FluentMediaPlayer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using NetDaemon.Common.Exceptions; namespace NetDaemon.Common.Fluent { @@ -63,8 +64,10 @@ public IMediaPlayerExecuteAsync PlayPause() /// public IMediaPlayerExecuteAsync Speak(string message) { - _currentAction = new FluentAction(FluentActionType.Speak); - _currentAction.MessageToSpeak = message; + _currentAction = new FluentAction(FluentActionType.Speak) + { + MessageToSpeak = message + }; return this; } @@ -78,7 +81,7 @@ public IMediaPlayerExecuteAsync Stop() /// public async Task ExecuteAsync() { - _ = _currentAction ?? throw new NullReferenceException("Missing fluent action type!"); + _ = _currentAction ?? throw new NetDaemonNullReferenceException("Missing fluent action type!"); var executeTask = _currentAction.ActionType switch { @@ -98,7 +101,7 @@ Task Speak() foreach (var entityId in EntityIds) { var message = _currentAction?.MessageToSpeak ?? - throw new NullReferenceException("Message to speak is null or empty!"); + throw new NetDaemonNullReferenceException("Message to speak is null or empty!"); Daemon.Speak(entityId, message); } diff --git a/src/App/NetDaemon.App/Common/IHttpHandler.cs b/src/App/NetDaemon.App/Common/IHttpHandler.cs index cf8c69ea7..1e848c5d1 100644 --- a/src/App/NetDaemon.App/Common/IHttpHandler.cs +++ b/src/App/NetDaemon.App/Common/IHttpHandler.cs @@ -1,4 +1,5 @@ -using System.Net.Http; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; @@ -7,6 +8,7 @@ namespace NetDaemon.Common /// /// Implements all Http features of NetDaemon /// + [SuppressMessage("", "CA1054")] public interface IHttpHandler { /// diff --git a/src/App/NetDaemon.App/Common/INetDaemon.cs b/src/App/NetDaemon.App/Common/INetDaemon.cs index 34e7550cb..315d09eab 100644 --- a/src/App/NetDaemon.App/Common/INetDaemon.cs +++ b/src/App/NetDaemon.App/Common/INetDaemon.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NetDaemon.Common.Fluent; @@ -103,8 +104,8 @@ public interface INetDaemon : INetDaemonCommon /// /// Selects the input selects to do actions on using lambda /// - /// The lambda expression selecting input select /// The Daemon App calling fluent API + /// The lambda expression selecting input select IFluentInputSelect InputSelects(INetDaemonApp app, Func func); /// @@ -147,6 +148,7 @@ public interface INetDaemon : INetDaemonCommon /// /// Base interface that all NetDaemon apps needs to implement /// + [SuppressMessage("", "CA1716")] public interface INetDaemonApp : INetDaemonAppBase { /// @@ -326,6 +328,7 @@ public interface INetDaemonApp : INetDaemonAppBase /// /// Shared features in both Reactive and async/await models /// + [SuppressMessage("", "CA1716")] public interface INetDaemonAppBase : INetDaemonInitialableApp, IDoLogging, IAsyncDisposable, IEquatable { @@ -361,7 +364,7 @@ public interface INetDaemonAppBase : /// /// Gets or sets a flag indicating whether this app is enabled. - /// This property property can be controlled from Home Assistant. + /// This property can be controlled from Home Assistant. /// /// /// A disabled app will not be initialized during the discovery. @@ -405,7 +408,6 @@ public interface INetDaemonAppBase : void ListenServiceCall(string domain, string service, Func action); - /// /// Returns different runtime information about an app /// @@ -595,6 +597,7 @@ public interface INetDaemonCommon /// exactly when it happens but it should be fine. The SetState function /// updates the state before sending. /// + [SuppressMessage("", "CA1721")] IEnumerable State { get; } /// @@ -602,7 +605,7 @@ public interface INetDaemonCommon /// /// Unique Id of the data /// The data persistent or null if not exists - ValueTask GetDataAsync(string id) where T : class; + Task GetDataAsync(string id) where T : class; /// /// Gets current state for the entity @@ -661,10 +664,11 @@ public interface INetDaemonInitialableApp /// /// /// + /// /// Restores the state of the storage object.!-- /// Todo: in the future also the state of tagged properties - /// - /// It is implemented async so state will be lazy saved + /// + /// It is implemented async so state will be lazy saved /// Task RestoreAppStateAsync(); @@ -672,10 +676,11 @@ public interface INetDaemonInitialableApp /// Saves the app state /// /// + /// /// Saves the state of the storage object.!-- /// Todo: in the future also the state of tagged properties - /// - /// It is implemented async so state will be lazy saved + /// + /// It is implemented async so state will be lazy saved /// void SaveAppState(); diff --git a/src/App/NetDaemon.App/Common/IScheduler.cs b/src/App/NetDaemon.App/Common/IScheduler.cs index 6ecdb4b83..3b1160b1b 100644 --- a/src/App/NetDaemon.App/Common/IScheduler.cs +++ b/src/App/NetDaemon.App/Common/IScheduler.cs @@ -22,8 +22,8 @@ public interface IScheduler : IAsyncDisposable /// Run daily tasks /// /// The time in the format HH:mm:ss - /// The action to run /// A list of days the scheduler will run on + /// The action to run /// ISchedulerResult RunDaily(string time, IEnumerable? runOnDays, Func func); diff --git a/src/App/NetDaemon.App/Common/Messages.cs b/src/App/NetDaemon.App/Common/Messages.cs index c9c9ad0e1..b30b2a50e 100644 --- a/src/App/NetDaemon.App/Common/Messages.cs +++ b/src/App/NetDaemon.App/Common/Messages.cs @@ -1,6 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; using System.Text.Json; using NetDaemon.Common.Fluent; @@ -14,52 +16,51 @@ public record Config /// /// Components being installed and used /// - public List? Components { get; init; } = null; + public IList? Components { get; } /// /// Local config directory /// - public string? ConfigDir { get; init; } = null; + public string? ConfigDir { get; } /// /// Elevation of Home Assistant instance /// - public int? Elevation { get; init; } = null; + public int? Elevation { get; } /// /// Latitude of Home Assistant instance /// - public float? Latitude { get; init; } = null; + public float? Latitude { get; } /// /// The name of the location /// - public string? LocationName { get; init; } = null; + public string? LocationName { get; } /// /// Longitude of Home Assistant instance /// - public float? Longitude { get; init; } = null; + public float? Longitude { get; } /// /// The timezone /// - public string? TimeZone { get; init; } = null; - + public string? TimeZone { get; } /// /// Unity system being configured /// - public HassUnitSystem? UnitSystem { get; init; } = null; + public HassUnitSystem? UnitSystem { get; init; } /// /// Current Home Assistant version /// - public string? Version { get; init; } = null; + public string? Version { get; init; } /// /// Whitelisted external directories /// - public List? WhitelistExternalDirs { get; init; } = null; + public ReadOnlyCollection? WhitelistExternalDirs { get; } } /// @@ -105,7 +106,6 @@ public record EntityState : IEntityProperties /// /// returns a pretty print of EntityState /// - /// public override string ToString() { string attributes = ""; @@ -115,7 +115,7 @@ public override string ToString() { if (key is not null) { - attributes = attributes + string.Format(" {0}:{1}", key, attr[key]); + attributes += string.Format(CultureInfo.InvariantCulture, " {0}:{1}", key, attr[key]); } } } @@ -131,7 +131,6 @@ public override string ToString() } } - /// /// Unit system parameters for Home Assistant /// @@ -140,22 +139,22 @@ public record HassUnitSystem /// /// Lenght unit /// - public string? Length { get; init; } = null; + public string? Length { get; init; } /// /// Mass unit /// - public string? Mass { get; init; } = null; + public string? Mass { get; init; } /// /// Temperature unit /// - public string? Temperature { get; init; } = null; + public string? Temperature { get; init; } /// /// Volume unit /// - public string? Volume { get; init; } = null; + public string? Volume { get; init; } } /// @@ -176,7 +175,6 @@ public record ServiceEvent /// /// Data being sent by the service /// - /// - public JsonElement? ServiceData { get; init; } = null; + public JsonElement? ServiceData { get; init; } } } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/NetDaemonApp.cs b/src/App/NetDaemon.App/Common/NetDaemonApp.cs index d1c6fcd9e..ab380b55a 100644 --- a/src/App/NetDaemon.App/Common/NetDaemonApp.cs +++ b/src/App/NetDaemon.App/Common/NetDaemonApp.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using NetDaemon.Common.Exceptions; using NetDaemon.Common.Fluent; [assembly: InternalsVisibleTo("NetDaemon.Daemon.Tests")] @@ -13,73 +15,70 @@ namespace NetDaemon.Common /// /// Base class för all NetDaemon apps /// + [SuppressMessage("", "CA1065"), + SuppressMessage("", "CA1721") + ] public abstract class NetDaemonApp : NetDaemonAppBase, INetDaemonApp, INetDaemonCommon { - private readonly IList<(string pattern, Func action)> _eventCallbacks = - new List<(string pattern, Func action)>(); - - private readonly List<(Func, Func)> _eventFunctionSelectorCallbacks = - new List<(Func, Func)>(); - private readonly ConcurrentDictionary action)> _stateCallbacks = - new ConcurrentDictionary action)>(); - /// /// All actions being performed for named events /// - public IList<(string pattern, Func action)> EventCallbacks => _eventCallbacks; + public IList<(string pattern, Func action)> EventCallbacks { get; } = + new List<(string pattern, Func action)>(); /// /// All actions being performed for lambda selected events /// - public List<(Func, Func)> EventFunctionCallbacks => _eventFunctionSelectorCallbacks; + public IList<(Func, Func)> EventFunctionCallbacks { get; } = new List<(Func, Func)>(); /// - public IScheduler Scheduler => _daemon?.Scheduler ?? - throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); + public IScheduler Scheduler => Daemon?.Scheduler ?? + throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); /// - public IEnumerable State => _daemon?.State ?? - throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); + public IEnumerable State => Daemon?.State ?? + throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); /// public ConcurrentDictionary action)> - StateCallbacks => _stateCallbacks; + StateCallbacks + { get; } = new(); // Used for testing - internal ConcurrentDictionary action)> InternalStateActions => _stateCallbacks; + internal ConcurrentDictionary action)> InternalStateActions => StateCallbacks; /// public Task CallService(string domain, string service, dynamic? data = null, bool waitForResponse = false) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.CallServiceAsync(domain, service, data, waitForResponse); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.CallServiceAsync(domain, service, data, waitForResponse); } /// public ICamera Camera(params string[] entityIds) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.Camera(this, entityIds); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.Camera(this, entityIds); } /// public ICamera Cameras(IEnumerable entityIds) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.Cameras(this, entityIds); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.Cameras(this, entityIds); } /// public ICamera Cameras(Func func) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.Cameras(this, func); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.Cameras(this, func); } /// public void CancelListenState(string id) { // Remove and ignore if not exist - _stateCallbacks.Remove(id, out _); + StateCallbacks.Remove(id, out _); } /// @@ -89,21 +88,26 @@ public void CancelListenState(string id) /// public IDelayResult DelayUntilStateChange(IEnumerable entityIds, object? to = null, object? from = null, bool allChanges = false) { + _ = entityIds ?? + throw new NetDaemonArgumentNullException(nameof(entityIds)); + // Use TaskCompletionSource to simulate a task that we can control var taskCompletionSource = new TaskCompletionSource(); var result = new DelayResult(taskCompletionSource, this); foreach (var entityId in entityIds) { - result.StateSubscriptions.Add(ListenState(entityId, (entityIdInn, newState, oldState) => + result.StateSubscriptions.Add(ListenState(entityId, (_, newState, oldState) => { - if (to != null) - if ((dynamic)to != newState?.State) - return Task.CompletedTask; + if (to != null && (dynamic)to != newState?.State) + { + return Task.CompletedTask; + } - if (from != null) - if ((dynamic)from != oldState?.State) - return Task.CompletedTask; + if (from != null && (dynamic)from != oldState?.State) + { + return Task.CompletedTask; + } // If we don´t accept all changes in the state change // and we do not have a state change so return @@ -123,15 +127,19 @@ public IDelayResult DelayUntilStateChange(IEnumerable entityIds, object? } /// + [SuppressMessage("", "CA1031")] public IDelayResult DelayUntilStateChange(IEnumerable entityIds, Func stateFunc) { + _ = entityIds ?? + throw new NetDaemonArgumentNullException(nameof(entityIds)); + // Use TaskCompletionSource to simulate a task that we can control var taskCompletionSource = new TaskCompletionSource(); var result = new DelayResult(taskCompletionSource, this); foreach (var entityId in entityIds) { - result.StateSubscriptions.Add(ListenState(entityId, (entityIdInn, newState, oldState) => + result.StateSubscriptions.Add(ListenState(entityId, (_, newState, oldState) => { try { @@ -161,31 +169,31 @@ public IDelayResult DelayUntilStateChange(IEnumerable entityIds, Func public async override ValueTask DisposeAsync() { - _stateCallbacks.Clear(); - _eventCallbacks.Clear(); - _eventFunctionSelectorCallbacks.Clear(); + StateCallbacks.Clear(); + EventCallbacks.Clear(); + EventFunctionCallbacks.Clear(); await base.DisposeAsync().ConfigureAwait(false); } /// public IEntity Entities(Func func) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.Entities(this, func); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.Entities(this, func); } /// - public IEntity Entities(IEnumerable entityIds) + public IEntity Entities(IEnumerable entityId) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.Entities(this, entityIds); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.Entities(this, entityId); } /// public IEntity Entity(params string[] entityId) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.Entity(this, entityId); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.Entity(this, entityId); } /// @@ -198,41 +206,41 @@ public IEntity Entity(params string[] entityId) public IFluentEvent Events(IEnumerable eventParams) => new FluentEventManager(eventParams, this); /// - public async ValueTask GetDataAsync(string id) where T : class + public async Task GetDataAsync(string id) where T : class { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return await _daemon!.GetDataAsync(id).ConfigureAwait(false); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return await Daemon!.GetDataAsync(id).ConfigureAwait(false); } /// - public EntityState? GetState(string entityId) => _daemon?.GetState(entityId); + public EntityState? GetState(string entityId) => Daemon?.GetState(entityId); /// public IFluentInputSelect InputSelect(params string[] inputSelectParams) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.InputSelect(this, inputSelectParams); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.InputSelect(this, inputSelectParams); } /// public IFluentInputSelect InputSelects(IEnumerable inputSelectParams) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.InputSelects(this, inputSelectParams); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.InputSelects(this, inputSelectParams); } /// public IFluentInputSelect InputSelects(Func func) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.InputSelects(this, func); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.InputSelects(this, func); } /// - public void ListenEvent(string ev, Func action) => _eventCallbacks.Add((ev, action)); + public void ListenEvent(string ev, Func action) => EventCallbacks.Add((ev, action)); /// - public void ListenEvent(Func funcSelector, Func func) => _eventFunctionSelectorCallbacks.Add((funcSelector, func)); + public void ListenEvent(Func funcSelector, Func action) => EventFunctionCallbacks.Add((funcSelector, action)); /// public string? ListenState(string pattern, @@ -241,57 +249,57 @@ public IFluentInputSelect InputSelects(Func func) // Use guid as unique id but will externally use string so // The design can change in-case guid won't cut it. var uniqueId = Guid.NewGuid().ToString(); - _stateCallbacks[uniqueId] = (pattern, action); + StateCallbacks[uniqueId] = (pattern, action); return uniqueId; } /// public IMediaPlayer MediaPlayer(params string[] entityIds) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.MediaPlayer(this, entityIds); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.MediaPlayer(this, entityIds); } /// public IMediaPlayer MediaPlayers(IEnumerable entityIds) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.MediaPlayers(this, entityIds); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.MediaPlayers(this, entityIds); } /// public IMediaPlayer MediaPlayers(Func func) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.MediaPlayers(this, func); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.MediaPlayers(this, func); } /// public IScript RunScript(params string[] entityIds) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.RunScript(this, entityIds); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.RunScript(this, entityIds); } /// public Task SaveDataAsync(string id, T data) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.SaveDataAsync(id, data); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.SaveDataAsync(id, data); } /// public async Task SendEvent(string eventId, dynamic? data = null) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return await _daemon!.SendEvent(eventId, data).ConfigureAwait(false); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return await Daemon!.SendEvent(eventId, data).ConfigureAwait(false); } /// public async Task SetStateAsync(string entityId, dynamic state, params (string name, object val)[] attributes) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return await _daemon!.SetStateAsync(entityId, state, attributes).ConfigureAwait(false); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return await Daemon!.SetStateAsync(entityId, state, attributes).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/NetDaemonAppBase.cs b/src/App/NetDaemon.App/Common/NetDaemonAppBase.cs index c546776ca..6bc0a2742 100644 --- a/src/App/NetDaemon.App/Common/NetDaemonAppBase.cs +++ b/src/App/NetDaemon.App/Common/NetDaemonAppBase.cs @@ -7,6 +7,7 @@ using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using NetDaemon.Common.Exceptions; using NetDaemon.Common.Fluent; namespace NetDaemon.Common @@ -19,45 +20,33 @@ public abstract class NetDaemonAppBase : INetDaemonAppBase /// /// A set of properties found in static analysis of code for each app /// - public static Dictionary> CompileTimeProperties { get; set; } = new(); + public static Dictionary> CompileTimeProperties { get; } = new(); - /// - /// The NetDaemonHost instance - /// - protected INetDaemon? _daemon; private Task? _manageRuntimeInformationUpdatesTask; - /// - /// Registered callbacks for service calls - /// - private readonly List<(string, string, Func)> _daemonCallBacksForServiceCalls - = new List<(string, string, Func)>(); - /// /// All actions being performed for service call events /// - public List<(string, string, Func)> DaemonCallBacksForServiceCalls => _daemonCallBacksForServiceCalls; - + public IList<(string, string, Func)> DaemonCallBacksForServiceCalls { get; } + = new List<(string, string, Func)>(); // This is declared as static since it will contain state shared globally - private static ConcurrentDictionary _global = new(); - - private readonly ConcurrentDictionary _attributes = new(); - - // To handle state saves max once at a time, internal due to tests - private readonly Channel _lazyStoreStateQueue = - Channel.CreateBounded(1); + private static readonly ConcurrentDictionary _global = new(); private readonly Channel _updateRuntimeInfoChannel = Channel.CreateBounded(5); - - // /// - // /// The last error message logged och catched - // /// - // public string? RuntimeInfo.LastErrorMessage { get; set; } = null; - private CancellationTokenSource _cancelSource = new CancellationTokenSource(); + private readonly CancellationTokenSource _cancelSource; + private bool _isDisposed; private Task? _lazyStoreStateTask; - private FluentExpandoObject? _storageObject; + + /// + /// Constructor + /// + protected NetDaemonAppBase() + { + _cancelSource = new(); + _isDisposed = false; + } /// /// Dependencies on other applications that will be initialized before this app @@ -68,12 +57,13 @@ public abstract class NetDaemonAppBase : INetDaemonAppBase public ConcurrentDictionary Global => _global; /// + [SuppressMessage("", "CA1065")] public IHttpHandler Http { get { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.Http; + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.Http; } } @@ -88,31 +78,29 @@ public string Description { get { - var app_key = this.GetType().FullName; + var app_key = GetType().FullName; if (app_key is null) return ""; - if (CompileTimeProperties.ContainsKey(app_key)) + if (CompileTimeProperties.ContainsKey(app_key) && CompileTimeProperties[app_key].ContainsKey("description")) { - if (CompileTimeProperties[app_key].ContainsKey("description")) - { - return CompileTimeProperties[app_key]["description"]; - } + return CompileTimeProperties[app_key]["description"]; } return ""; } } - /// public ILogger? Logger { get; set; } /// - public dynamic Storage => _storageObject ?? throw new NullReferenceException($"{nameof(_storageObject)} cant be null"); + [SuppressMessage("", "CA1065")] + public dynamic Storage => InternalStorageObject ?? throw new NetDaemonNullReferenceException($"{nameof(InternalStorageObject)} cant be null"); - internal Channel InternalLazyStoreStateQueue => _lazyStoreStateQueue; - internal FluentExpandoObject? InternalStorageObject { get { return _storageObject; } set { _storageObject = value; } } + internal Channel InternalLazyStoreStateQueue { get; } = + Channel.CreateBounded(1); + internal FluentExpandoObject? InternalStorageObject { get; set; } /// /// Implements the IEqualit.Equals method @@ -120,10 +108,7 @@ public string Description /// The instance to compare public bool Equals([AllowNull] INetDaemonAppBase other) { - if (other is not null && other.Id is not null && this.Id is not null && this.Id == other.Id) - return true; - - return false; + return other is not null && other.Id is not null && Id is not null && Id == other.Id; } /// @@ -146,9 +131,9 @@ public virtual Task InitializeAsync() /// public async Task RestoreAppStateAsync() { - _ = _daemon as INetDaemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); - var obj = await _daemon!.GetDataAsync>(GetUniqueIdForStorage()).ConfigureAwait(false); + var obj = await Daemon.GetDataAsync>(GetUniqueIdForStorage()).ConfigureAwait(false); if (obj != null) { @@ -156,8 +141,8 @@ public async Task RestoreAppStateAsync() expStore.CopyFrom(obj); } - bool isDisabled = this.Storage.__IsDisabled ?? false; - var appInfo = _daemon!.State.FirstOrDefault(s => s.EntityId == EntityId); + bool isDisabled = Storage.__IsDisabled ?? false; + var appInfo = Daemon!.State.FirstOrDefault(s => s.EntityId == EntityId); var appState = appInfo?.State as string; if (isDisabled) @@ -167,8 +152,7 @@ public async Task RestoreAppStateAsync() { dynamic serviceData = new FluentExpandoObject(); serviceData.entity_id = EntityId; - await _daemon.SetStateAsync(EntityId, "off").ConfigureAwait(false); - await _daemon.CallServiceAsync("switch", "turn_off", serviceData); + await Daemon.SetStateAsync(EntityId, "off").ConfigureAwait(false); } return; } @@ -179,8 +163,7 @@ public async Task RestoreAppStateAsync() { dynamic serviceData = new FluentExpandoObject(); serviceData.entity_id = EntityId; - await _daemon.SetStateAsync(EntityId, "on").ConfigureAwait(false); - await _daemon.CallServiceAsync("switch", "turn_on", serviceData); + await Daemon.SetStateAsync(EntityId, "on").ConfigureAwait(false); } return; } @@ -189,14 +172,18 @@ public async Task RestoreAppStateAsync() /// public string EntityId => $"switch.netdaemon_{Id?.ToSafeHomeAssistantEntityId()}"; - AppRuntimeInfo _runtimeInfo = new AppRuntimeInfo { HasError = false }; - /// - public AppRuntimeInfo RuntimeInfo => _runtimeInfo; + public AppRuntimeInfo RuntimeInfo { get; } = new AppRuntimeInfo { HasError = false }; /// - public IEnumerable EntityIds => _daemon?.State.Select(n => n.EntityId) ?? - throw new NullReferenceException("Deamon not expected to be null"); + [SuppressMessage("", "CA1065")] + public IEnumerable EntityIds => Daemon?.State.Select(n => n.EntityId) ?? + throw new NetDaemonNullReferenceException("Deamon not expected to be null"); + + /// + /// Instance to Daemon service + /// + protected INetDaemon? Daemon { get; set; } /// public void SaveAppState() @@ -204,30 +191,31 @@ public void SaveAppState() // Intentionally ignores full queue since we know // a state change already is in progress wich means // this state will be saved - var x = _lazyStoreStateQueue.Writer.TryWrite(true); + _ = InternalLazyStoreStateQueue.Writer.TryWrite(true); } /// public void Speak(string entityId, string message) { - _ = _daemon as INetDaemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - _daemon!.Speak(entityId, message); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + Daemon!.Speak(entityId, message); } /// public virtual Task StartUpAsync(INetDaemon daemon) { - _daemon = daemon; + _ = daemon ?? throw new NetDaemonArgumentNullException(nameof(daemon)); + + Daemon = daemon; _manageRuntimeInformationUpdatesTask = ManageRuntimeInformationUpdates(); _lazyStoreStateTask = Task.Run(async () => await HandleLazyStorage().ConfigureAwait(false)); - _storageObject = new FluentExpandoObject(false, true, daemon: this); + InternalStorageObject = new FluentExpandoObject(false, true, daemon: this); Logger = daemon.Logger; Logger.LogDebug("Startup: {app}", GetUniqueIdForStorage()); - var appInfo = _daemon!.State.FirstOrDefault(s => s.EntityId == EntityId); - var appState = appInfo?.State as string; - if (appState == null || (appState != "on" && appState != "off")) + var appInfo = Daemon!.State.FirstOrDefault(s => s.EntityId == EntityId); + if (appInfo?.State is not string appState || (appState != "on" && appState != "off")) { IsEnabled = true; } @@ -242,22 +230,24 @@ public virtual Task StartUpAsync(INetDaemon daemon) /// /// Returns unique Id for instance /// - public string GetUniqueIdForStorage() => $"{this.GetType().Name}_{Id}".ToLowerInvariant(); + [SuppressMessage("Microsoft.Globalization", "CA1308")] + public string GetUniqueIdForStorage() => $"{GetType().Name}_{Id}".ToLowerInvariant(); + [SuppressMessage("Microsoft.Design", "CA1031")] private async Task HandleLazyStorage() { - _ = _storageObject ?? - throw new NullReferenceException($"{nameof(_storageObject)} cant be null!"); - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); + _ = InternalStorageObject ?? + throw new NetDaemonNullReferenceException($"{nameof(InternalStorageObject)} cant be null!"); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); while (!_cancelSource.IsCancellationRequested) { try { // Dont care about the result, just that it is time to store state - _ = await _lazyStoreStateQueue.Reader.ReadAsync(_cancelSource.Token); + _ = await InternalLazyStoreStateQueue.Reader.ReadAsync(_cancelSource.Token).ConfigureAwait(false); - await _daemon!.SaveDataAsync>(GetUniqueIdForStorage(), (IDictionary)Storage) + await Daemon!.SaveDataAsync(GetUniqueIdForStorage(), (IDictionary)Storage) .ConfigureAwait(false); } catch (OperationCanceledException) @@ -278,29 +268,37 @@ private async Task HandleLazyStorage() /// public async virtual ValueTask DisposeAsync() { + lock (_cancelSource) + { + if (_isDisposed) + return; + _isDisposed = true; + } _cancelSource.Cancel(); if (_manageRuntimeInformationUpdatesTask is not null) await _manageRuntimeInformationUpdatesTask.ConfigureAwait(false); - _daemonCallBacksForServiceCalls.Clear(); + DaemonCallBacksForServiceCalls.Clear(); - this.IsEnabled = false; + IsEnabled = false; _lazyStoreStateTask = null; - _storageObject = null; - _daemon = null; - + InternalStorageObject = null; + _cancelSource.Dispose(); + Daemon = null; } /// public INetDaemonAppBase? GetApp(string appInstanceId) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon!.GetApp(appInstanceId); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon!.GetApp(appInstanceId); } /// + [SuppressMessage("Microsoft.Design", "CA1062")] + [SuppressMessage("Microsoft.Design", "CA1308")] public void ListenServiceCall(string domain, string service, Func action) - => _daemonCallBacksForServiceCalls.Add((domain.ToLowerInvariant(), service.ToLowerInvariant(), action)); + => DaemonCallBacksForServiceCalls.Add((domain.ToLowerInvariant(), service.ToLowerInvariant(), action)); /// public void Log(string message) => Log(LogLevel.Information, message); @@ -311,7 +309,12 @@ public void ListenServiceCall(string domain, string service, Func public void Log(LogLevel level, string message, params object[] param) { - if (param is not null && param.Length > 0) + if (Logger is null) + { + return; + } + + if (param.Length > 0) { var result = param.Prepend(Id).ToArray(); Logger.Log(level, $" {{Id}}: {message}", result); @@ -328,7 +331,7 @@ public void Log(LogLevel level, string message, params object[] param) /// public void Log(LogLevel level, Exception exception, string message, params object[] param) { - if (param is not null && param.Length > 0) + if (param.Length > 0) { var result = param.Prepend(Id).ToArray(); Logger.Log(level, exception, $" {{Id}}: {message}", result); @@ -462,7 +465,7 @@ private async Task ManageRuntimeInformationUpdates() { while (_updateRuntimeInfoChannel.Reader.TryRead(out _)) ; - _ = await _updateRuntimeInfoChannel.Reader.ReadAsync(_cancelSource.Token); + _ = await _updateRuntimeInfoChannel.Reader.ReadAsync(_cancelSource.Token).ConfigureAwait(false); // do the deed await HandleUpdateRuntimeInformation().ConfigureAwait(false); // make sure we never push more messages that 10 per second @@ -478,16 +481,14 @@ private async Task ManageRuntimeInformationUpdates() private async Task HandleUpdateRuntimeInformation() { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); if (RuntimeInfo.LastErrorMessage is not null) { RuntimeInfo.HasError = true; } - await _daemon!.SetStateAsync(EntityId, IsEnabled ? "on" : "off", ("runtime_info", RuntimeInfo)).ConfigureAwait(false); + await Daemon!.SetStateAsync(EntityId, IsEnabled ? "on" : "off", ("runtime_info", RuntimeInfo)).ConfigureAwait(false); } - - } } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs b/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs index 24537d81b..3d3ede231 100644 --- a/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs +++ b/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reactive.Concurrency; @@ -8,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using NetDaemon.Common.Exceptions; using NetDaemon.Common.Fluent; // For mocking @@ -20,15 +22,24 @@ namespace NetDaemon.Common.Reactive /// public abstract class NetDaemonRxApp : NetDaemonAppBase, INetDaemonReactive { - private CancellationTokenSource _cancelTimers = new CancellationTokenSource(); + private readonly CancellationTokenSource _cancelTimers; private EventObservable? _eventObservables; - private ReactiveEvent? _reactiveEvent = null; - private ReactiveState? _reactiveState = null; private StateChangeObservable? _stateObservables; + private bool _isDisposed; + + /// + /// Default constructor + /// + protected NetDaemonRxApp() + { + _cancelTimers = new(); + StateAllChanges = new ReactiveState(this); + EventChanges = new ReactiveEvent(this); + _isDisposed = false; + } /// - public IRxEvent EventChanges => - _reactiveEvent ?? throw new ApplicationException("Application not initialized correctly (EventChanges)"); + public IRxEvent EventChanges { get; } /// /// Returns the observables events implementation of AppDaemonRxApps @@ -36,12 +47,10 @@ public abstract class NetDaemonRxApp : NetDaemonAppBase, INetDaemonReactive public ObservableBase EventChangesObservable => _eventObservables!; /// - public IObservable<(EntityState Old, EntityState New)> StateAllChanges => - _reactiveState ?? throw new ApplicationException("Application not initialized correctly (StateAllChanges>"); + public IObservable<(EntityState Old, EntityState New)> StateAllChanges { get; } /// - public IObservable<(EntityState Old, EntityState New)> StateChanges => - _reactiveState?.Where(e => e.New?.State != e.Old?.State) ?? throw new ApplicationException("Application not initialized correctly (StateAllChanges>"); + public IObservable<(EntityState Old, EntityState New)> StateChanges => StateAllChanges.Where(e => e.New?.State != e.Old?.State); /// /// Returns the observables states implementation of AppDaemonRxApps @@ -50,13 +59,13 @@ public abstract class NetDaemonRxApp : NetDaemonAppBase, INetDaemonReactive /// public IEnumerable States => - _daemon?.State ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); + Daemon?.State ?? new List(); /// public void CallService(string domain, string service, dynamic? data) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - _daemon.CallService(domain, service, data); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + Daemon.CallService(domain, service, data); } /// @@ -64,8 +73,16 @@ public void CallService(string domain, string service, dynamic? data) /// public async override ValueTask DisposeAsync() { + lock (_cancelTimers) + { + if (_isDisposed) + return; + _isDisposed = true; + } + + LogDebug("RxApp {app} is being Disposes", Id!); // To end timers - _cancelTimers.Cancel(); + _cancelTimers?.Cancel(); if (_eventObservables is not null) _eventObservables!.Clear(); @@ -73,10 +90,7 @@ public async override ValueTask DisposeAsync() if (_stateObservables is not null) _stateObservables!.Clear(); - // Make sure we release all references so the apps can be - // unloaded correctly - _reactiveEvent = null; - _reactiveState = null; + _cancelTimers?.Dispose(); await base.DisposeAsync().ConfigureAwait(false); LogDebug("RxApp {app} is Disposed", Id!); @@ -85,17 +99,17 @@ public async override ValueTask DisposeAsync() /// public RxEntity Entities(Func func) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); try { - IEnumerable x = _daemon.State.Where(func); + IEnumerable x = Daemon.State.Where(func); return new RxEntity(this, x.Select(n => n.EntityId).ToArray()); } catch (Exception e) { - _daemon.Logger.LogDebug(e, "Failed to select entities func in app {appId}", Id); + Daemon.Logger.LogDebug(e, "Failed to select entities func in app {appId}", Id); throw; } } @@ -106,30 +120,28 @@ public RxEntity Entities(Func func) /// public RxEntity Entities(IEnumerable entityIds) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); return new RxEntity(this, entityIds); } /// public RxEntity Entity(string entityId) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); return new RxEntity(this, new string[] { entityId }); } /// public T? GetData(string id) where T : class { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - return _daemon.GetDataAsync(id).Result; + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + return Daemon.GetDataAsync(id).Result; } /// public IDisposable RunDaily(string time, Action action) { - DateTime parsedTime; - - if (!DateTime.TryParseExact(time, "HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedTime)) + if (!DateTime.TryParseExact(time, "HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsedTime)) { throw new FormatException($"{time} is not a valid time for the current locale"); } @@ -144,8 +156,10 @@ public IDisposable RunDaily(string time, Action action) ); if (now > timeOfDayToTrigger) + { // It is not due until tomorrow timeOfDayToTrigger = timeOfDayToTrigger.AddDays(1); + } return CreateObservableTimer(timeOfDayToTrigger, TimeSpan.FromDays(1), action); } @@ -159,10 +173,9 @@ public IDisposable RunEvery(TimeSpan timespan, Action action) /// public IDisposable RunEveryHour(string time, Action action) { - DateTime parsedTime; time = $"{DateTime.Now.Hour:D2}:{time}"; - if (!DateTime.TryParseExact(time, "HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedTime)) + if (!DateTime.TryParseExact(time, "HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsedTime)) { throw new FormatException($"{time} is not a valid time for the current locale"); } @@ -178,8 +191,10 @@ public IDisposable RunEveryHour(string time, Action action) ); if (now > timeOfDayToTrigger) + { // It is not due until tomorrow timeOfDayToTrigger = timeOfDayToTrigger.AddHours(1); + } return CreateObservableTimer(timeOfDayToTrigger, TimeSpan.FromHours(1), action); } @@ -194,16 +209,17 @@ public IDisposable RunEveryMinute(short second, Action action) } /// + [SuppressMessage("", "CA1031")] // In this case we want just to log the message public IDisposable RunIn(TimeSpan timespan, Action action) { var result = new DisposableTimerResult(_cancelTimers.Token); Observable.Timer(timespan, TaskPoolScheduler.Default) .Subscribe( - s => + _ => { try { - if (this.IsEnabled) + if (IsEnabled) action(); } catch (OperationCanceledException) @@ -223,53 +239,52 @@ public IDisposable RunIn(TimeSpan timespan, Action action) /// public void RunScript(params string[] script) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); foreach (var scriptName in script) { var name = scriptName; - if (scriptName.Contains('.')) - name = scriptName[(scriptName.IndexOf('.') + 1)..]; + if (scriptName.Contains('.', StringComparison.InvariantCultureIgnoreCase)) + name = scriptName[(scriptName.IndexOf('.', StringComparison.InvariantCultureIgnoreCase) + 1)..]; - _daemon.CallService("script", name); + Daemon.CallService("script", name); } } /// public void SaveData(string id, T data) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - _daemon.SaveDataAsync(id, data).Wait(); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + Daemon.SaveDataAsync(id, data).Wait(); } /// public void SetState(string entityId, dynamic state, dynamic? attributes = null) { - _ = _daemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - _daemon.SetState(entityId, state, attributes); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + Daemon.SetState(entityId, state, attributes); } /// public async override Task StartUpAsync(INetDaemon daemon) { - await base.StartUpAsync(daemon); - _ = _daemon as INetDaemon ?? throw new NullReferenceException($"{nameof(_daemon)} cant be null!"); - _ = Logger ?? throw new NullReferenceException("Logger can not be null!"); + await base.StartUpAsync(daemon).ConfigureAwait(false); + _ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!"); + _ = Logger ?? throw new NetDaemonNullReferenceException("Logger can not be null!"); _eventObservables = new EventObservable(Logger, this); _stateObservables = new StateChangeObservable(Logger, this); - _reactiveState = new ReactiveState(this); - _reactiveEvent = new ReactiveEvent(this); } /// - public EntityState? State(string entityId) => _daemon?.GetState(entityId); + public EntityState? State(string entityId) => Daemon?.GetState(entityId); /// /// Creates an observable intervall /// /// Time span for intervall /// The action to call + [SuppressMessage("Microsoft.Design", "CA1031")] internal virtual IDisposable CreateObservableIntervall(TimeSpan timespan, Action action) { var result = new DisposableTimerResult(_cancelTimers.Token); @@ -278,11 +293,11 @@ internal virtual IDisposable CreateObservableIntervall(TimeSpan timespan, Action Observable.Interval(timespan, TaskPoolScheduler.Default) .Subscribe( - s => + _ => { try { - if (this.IsEnabled) + if (IsEnabled) { action(); RuntimeInfo.NextScheduledEvent = DateTime.Now + timespan; @@ -298,10 +313,7 @@ internal virtual IDisposable CreateObservableIntervall(TimeSpan timespan, Action LogError(e, "Error, ObservableIntervall APP: {app}", Id ?? "unknown"); } }, - ex => - { - LogTrace("Exiting ObservableIntervall for app {app}, {trigger}:{span}", Id!, timespan); - } + ex => LogTrace(ex, "Exiting ObservableIntervall for app {app}, {trigger}:{span}", Id!, timespan) , result.Token); return result; @@ -313,7 +325,8 @@ internal virtual IDisposable CreateObservableIntervall(TimeSpan timespan, Action /// When to start the timer /// The intervall /// Action to call each intervall - /// + [SuppressMessage("Microsoft.Design", "CA1031")] + internal virtual IDisposable CreateObservableTimer(DateTime timeOfDayToTrigger, TimeSpan interval, Action action) { var result = new DisposableTimerResult(_cancelTimers.Token); @@ -326,11 +339,11 @@ internal virtual IDisposable CreateObservableTimer(DateTime timeOfDayToTrigger, interval, TaskPoolScheduler.Default) .Subscribe( - s => + _ => { try { - if (this.IsEnabled) + if (IsEnabled) { action(); RuntimeInfo.NextScheduledEvent = DateTime.Now + interval; @@ -349,7 +362,7 @@ internal virtual IDisposable CreateObservableTimer(DateTime timeOfDayToTrigger, () => Log("Exiting timer for app {app}, {trigger}:{span}", Id!, timeOfDayToTrigger, interval), result.Token - ); + ); return result; } diff --git a/src/App/NetDaemon.App/Common/Reactive/DisposableTimerResult.cs b/src/App/NetDaemon.App/Common/Reactive/DisposableTimerResult.cs index faa65eae9..4b9491374 100644 --- a/src/App/NetDaemon.App/Common/Reactive/DisposableTimerResult.cs +++ b/src/App/NetDaemon.App/Common/Reactive/DisposableTimerResult.cs @@ -30,7 +30,8 @@ public DisposableTimerResult(CancellationToken token) public void Dispose() { _internalToken.Cancel(); + _combinedToken.Dispose(); + _internalToken.Dispose(); } } - } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/Reactive/ObservableBase.cs b/src/App/NetDaemon.App/Common/Reactive/ObservableBase.cs index fec144b97..5a9c59d11 100644 --- a/src/App/NetDaemon.App/Common/Reactive/ObservableBase.cs +++ b/src/App/NetDaemon.App/Common/Reactive/ObservableBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; namespace NetDaemon.Common.Reactive @@ -17,7 +18,7 @@ public class ObservableBase : IObservable private readonly ILogger _logger; private readonly ConcurrentDictionary, IObserver> - _observersTuples = new ConcurrentDictionary, IObserver>(); + _observersTuples = new(); /// /// Constructor /// @@ -36,6 +37,8 @@ public ObservableBase(ILogger logger, INetDaemonAppBase app) /// /// Clear all observers /// + [SuppressMessage("Microsoft.Design", "CA1031")] + public void Clear() { foreach (var eventObservable in _observersTuples) @@ -75,8 +78,8 @@ private class UnsubscriberObservable : IDisposable public UnsubscriberObservable( ConcurrentDictionary, IObserver> observers, IObserver observer) { - this._observers = observers; - this._observer = observer; + _observer = observer; + _observers = observers; } public void Dispose() @@ -86,7 +89,6 @@ public void Dispose() _observers.TryRemove(_observer, out _); _observer.OnCompleted(); } - // System.Console.WriteLine($"Subscribers:{_observers.Count}"); } } } diff --git a/src/App/NetDaemon.App/Common/Reactive/ObservableExtensionMethods.cs b/src/App/NetDaemon.App/Common/Reactive/ObservableExtensionMethods.cs index 938726c2b..d26a96e65 100644 --- a/src/App/NetDaemon.App/Common/Reactive/ObservableExtensionMethods.cs +++ b/src/App/NetDaemon.App/Common/Reactive/ObservableExtensionMethods.cs @@ -13,7 +13,6 @@ public static class ObservableExtensionMethods /// /// /// - /// public static IObservable<(EntityState Old, EntityState New)> NDSameStateFor(this IObservable<(EntityState Old, EntityState New)> observable, TimeSpan span) { return observable.Throttle(span); @@ -26,17 +25,17 @@ public static IObservable<(EntityState Old, EntityState New)> NDSameStateFor(thi /// Timeout waiting for state public static IObservable<(EntityState Old, EntityState New)> NDWaitForState(this IObservable<(EntityState Old, EntityState New)> observable, TimeSpan timeout) { - return observable.Timeout(timeout, Observable.Return((new NetDaemon.Common.EntityState() { State = "TimeOut" }, new NetDaemon.Common.EntityState() { State = "TimeOut" }))).Take(1); + return observable + .Timeout(timeout, + Observable.Return((new EntityState() { State = "TimeOut" }, new EntityState() { State = "TimeOut" }))).Take(1); } /// /// Wait for state the default time /// /// - public static IObservable<(EntityState Old, EntityState New)> NDWaitForState(this IObservable<(EntityState Old, EntityState New)> observable) - { - return observable.Timeout(TimeSpan.FromSeconds(5), Observable.Return((new NetDaemon.Common.EntityState() { State = "TimeOut" }, new NetDaemon.Common.EntityState() { State = "TimeOut" }))).Take(1); - } - + public static IObservable<(EntityState Old, EntityState New)> NDWaitForState(this IObservable<(EntityState Old, EntityState New)> observable) => observable + .Timeout(TimeSpan.FromSeconds(5), + Observable.Return((new EntityState() { State = "TimeOut" }, new EntityState() { State = "TimeOut" }))).Take(1); } } \ No newline at end of file diff --git a/src/App/NetDaemon.App/Common/Reactive/RxEntity.cs b/src/App/NetDaemon.App/Common/Reactive/RxEntity.cs index 10118100b..cdc827b63 100644 --- a/src/App/NetDaemon.App/Common/Reactive/RxEntity.cs +++ b/src/App/NetDaemon.App/Common/Reactive/RxEntity.cs @@ -3,6 +3,7 @@ using System.Dynamic; using System.Linq; using System.Reactive.Linq; +using NetDaemon.Common.Exceptions; using NetDaemon.Common.Fluent; namespace NetDaemon.Common.Reactive @@ -68,11 +69,11 @@ public class RxEntity : ICanTurnOnAndOff, ISetState, IObserve /// /// The protected daemon app instance /// - protected readonly INetDaemonReactive DaemonRxApp; + protected INetDaemonReactive DaemonRxApp { get; } /// /// Entity ids being handled by the RxEntity /// - protected readonly IEnumerable EntityIds; + protected IEnumerable EntityIds { get; } /// /// Constructor @@ -108,7 +109,6 @@ public void SetState(dynamic state, dynamic? attributes = null) { foreach (var entityId in EntityIds) { - var domain = GetDomainFromEntity(entityId); DaemonRxApp.SetState(entityId, state, attributes); } } @@ -126,7 +126,7 @@ internal static string GetDomainFromEntity(string entity) { var entityParts = entity.Split('.'); if (entityParts.Length != 2) - throw new ApplicationException($"entity_id is mal formatted {entity}"); + throw new NetDaemonException($"entity_id is mal formatted {entity}"); return entityParts[0]; } @@ -138,7 +138,7 @@ internal static string GetDomainFromEntity(string entity) /// Data to provide public void CallService(string service, dynamic? data = null) { - if (EntityIds is null || EntityIds is not null && EntityIds.Count() == 0) + if (EntityIds?.Any() != true) return; foreach (var entityId in EntityIds!) @@ -170,14 +170,14 @@ public void CallService(string service, dynamic? data = null) private void CallServiceOnEntity(string service, dynamic? attributes = null) { - if (EntityIds is null || EntityIds is not null && EntityIds.Count() == 0) + if (EntityIds?.Any() != true) return; dynamic? data = null; if (attributes is not null) { - if (attributes is IDictionary == false) + if (attributes is not IDictionary) data = ((object)attributes).ToExpandoObject(); else data = attributes; diff --git a/src/App/NetDaemon.App/Common/Reactive/RxEvent.cs b/src/App/NetDaemon.App/Common/Reactive/RxEvent.cs index 327a74b9c..7b2de97f7 100644 --- a/src/App/NetDaemon.App/Common/Reactive/RxEvent.cs +++ b/src/App/NetDaemon.App/Common/Reactive/RxEvent.cs @@ -3,11 +3,9 @@ /// /// Represent an event from eventstream /// - public struct RxEvent + public class RxEvent { - private readonly dynamic? _data; private readonly string? _domain; - private readonly string _eventName; /// /// Constructor @@ -17,15 +15,15 @@ public struct RxEvent /// Data public RxEvent(string eventName, string? domain, dynamic? data) { - _eventName = eventName; + Event = eventName; _domain = domain; - _data = data; + Data = data; } /// /// Data from event /// - public dynamic? Data => _data; + public dynamic? Data { get; } /// /// Domain (call service event) @@ -35,6 +33,6 @@ public RxEvent(string eventName, string? domain, dynamic? data) /// /// The event being sent /// - public string Event => _eventName; + public string Event { get; } } } \ No newline at end of file diff --git a/src/App/NetDaemon.App/NetDaemon.App.csproj b/src/App/NetDaemon.App/NetDaemon.App.csproj index 72f6946b4..927ae709e 100644 --- a/src/App/NetDaemon.App/NetDaemon.App.csproj +++ b/src/App/NetDaemon.App/NetDaemon.App.csproj @@ -24,7 +24,15 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - + + ..\..\..\.linting\roslynator.ruleset + true + AllEnabledByDefault + diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/CodeManager.cs b/src/Daemon/NetDaemon.Daemon/Daemon/CodeManager.cs index c6079cfa9..4c1615908 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/CodeManager.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/CodeManager.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; using NetDaemon.Common; +using NetDaemon.Common.Exceptions; using NetDaemon.Daemon.Config; [assembly: InternalsVisibleTo("NetDaemon.Daemon.Tests")] @@ -15,7 +17,7 @@ public sealed class CodeManager : IInstanceDaemonApp { private readonly ILogger _logger; private readonly IYamlConfig _yamlConfig; - private IEnumerable? _loadedDaemonApps; + private readonly IEnumerable? _loadedDaemonApps; /// /// Constructor @@ -30,7 +32,8 @@ public CodeManager(IEnumerable daemonAppTypes, ILogger logger, IYamlConfig _yamlConfig = yamlConfig; } - public int Count => _loadedDaemonApps?.Count() ?? throw new NullReferenceException("_loadedDaemonApps cannot be null"); + [SuppressMessage("", "CA1065")] + public int Count => _loadedDaemonApps?.Count() ?? throw new NetDaemonNullReferenceException("_loadedDaemonApps cannot be null"); // Internal for testing public IEnumerable DaemonAppTypes => _loadedDaemonApps!; @@ -40,7 +43,7 @@ public IEnumerable InstanceDaemonApps() var result = new List(50); // No loaded, just return an empty list - if (_loadedDaemonApps is null || _loadedDaemonApps.Count() == 0) + if (_loadedDaemonApps?.Any() != true) return result; // Get all yaml config file paths @@ -56,7 +59,8 @@ public IEnumerable InstanceDaemonApps() { try { - var yamlAppConfig = new YamlAppConfig(_loadedDaemonApps, File.OpenText(file), _yamlConfig, file); + using var fileReader = File.OpenText(file); + var yamlAppConfig = new YamlAppConfig(_loadedDaemonApps, fileReader, _yamlConfig, file); foreach (var appInstance in yamlAppConfig.Instances) { @@ -67,7 +71,7 @@ public IEnumerable InstanceDaemonApps() { _logger.LogTrace(e, "Error instance the app from the file {file}", file); _logger.LogError("Error instance the app from the file {file}, use trace flag for details", file); - throw new ApplicationException($"Error instance the app from the file {file}", e); + throw new NetDaemonException($"Error instance the app from the file {file}", e); } } return result; diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/Config/ConfigExtensions.cs b/src/Daemon/NetDaemon.Daemon/Daemon/Config/ConfigExtensions.cs index 532e45605..f413e67c6 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/Config/ConfigExtensions.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/Config/ConfigExtensions.cs @@ -1,7 +1,9 @@ -using System.Reflection; +using System.Globalization; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using NetDaemon.Common.Exceptions; [assembly: InternalsVisibleTo("NetDaemon.Daemon.Tests")] @@ -15,6 +17,9 @@ public static class TaskExtensions { public static async Task InvokeAsync(this MethodInfo mi, object? obj, params object?[]? parameters) { + _ = mi ?? + throw new NetDaemonArgumentNullException(nameof(mi)); + dynamic? awaitable = mi.Invoke(obj, parameters); if (awaitable != null) await awaitable.ConfigureAwait(false); @@ -25,21 +30,27 @@ public static class ConfigStringExtensions { public static string ToPythonStyle(this string str) { + _ = str ?? + throw new NetDaemonArgumentNullException(nameof(str)); + var build = new StringBuilder(str.Length); bool isStart = true; foreach (char c in str) { if (char.IsUpper(c) && !isStart) - build.Append("_"); + build.Append('_'); else isStart = false; - build.Append(char.ToLower(c)); + build.Append(char.ToLower(c, CultureInfo.InvariantCulture)); } return build.ToString(); } public static string ToCamelCase(this string str) { + _ = str ?? + throw new NetDaemonArgumentNullException(nameof(str)); + var build = new StringBuilder(); bool nextIsUpper = false; bool isFirstCharacter = true; @@ -51,11 +62,10 @@ public static string ToCamelCase(this string str) continue; } - build.Append(nextIsUpper || isFirstCharacter ? char.ToUpper(c) : c); + build.Append(nextIsUpper || isFirstCharacter ? char.ToUpper(c, CultureInfo.InvariantCulture) : c); nextIsUpper = false; isFirstCharacter = false; } - var returnString = build.ToString(); return build.ToString(); } diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlAppConfig.cs b/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlAppConfig.cs index 1b72b1257..6cb10e8b2 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlAppConfig.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlAppConfig.cs @@ -1,9 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using NetDaemon.Common; +using NetDaemon.Common.Exceptions; using YamlDotNet.RepresentationModel; namespace NetDaemon.Daemon.Config @@ -25,6 +27,8 @@ public YamlAppConfig(IEnumerable types, TextReader reader, IYamlConfig yam _yamlFilePath = yamlFilePath; } + [SuppressMessage("", "CA1508")] + [SuppressMessage("", "CA1065")] public IEnumerable Instances { get @@ -46,12 +50,11 @@ public IEnumerable Instances // Get the class string? appClass = GetTypeNameFromClassConfig((YamlMappingNode)app.Value); - Type? appType = _types.Where(n => n.FullName?.ToLowerInvariant() == appClass) - .FirstOrDefault(); + Type? appType = _types.FirstOrDefault(n => n.FullName?.ToLowerInvariant() == appClass); if (appType != null) { - var instance = InstanceAndSetPropertyConfig(appType, ((YamlMappingNode)app.Value), appId); + var instance = InstanceAndSetPropertyConfig(appType, (YamlMappingNode)app.Value, appId); if (instance != null) { instance.Id = appId; @@ -59,9 +62,9 @@ public IEnumerable Instances } } } - catch (System.Exception e) + catch (Exception e) { - throw new ApplicationException($"Error instancing application {appId}", e); + throw new NetDaemonException($"Error instancing application {appId}", e); } } @@ -69,12 +72,15 @@ public IEnumerable Instances } } - public INetDaemonAppBase? InstanceAndSetPropertyConfig(Type netDaemonAppType, YamlMappingNode appNode, string? appId) + public INetDaemonAppBase? InstanceAndSetPropertyConfig( + Type netDaemonAppType, + YamlMappingNode appNode, + string? appId) { - var netDaemonApp = (INetDaemonAppBase?)Activator.CreateInstance(netDaemonAppType); + _ = appNode ?? + throw new NetDaemonArgumentNullException(nameof(appNode)); - if (netDaemonApp == null) - return null; + var netDaemonApp = (INetDaemonAppBase?)Activator.CreateInstance(netDaemonAppType); foreach (KeyValuePair entry in appNode.Children) { @@ -96,16 +102,16 @@ public IEnumerable Instances } catch (Exception e) { - throw new ApplicationException($"Failed to set value {scalarPropertyName}", e); + throw new NetDaemonException($"Failed to set value {scalarPropertyName} for app {appId}", e); } } return netDaemonApp; } - private object? InstanceProperty(Object? parent, Type instanceType, YamlNode node) + [SuppressMessage("", "CA1508")] // Weird bug that this should not warn! + private object? InstanceProperty(object? parent, Type instanceType, YamlNode node) { - if (node.NodeType == YamlNodeType.Scalar) { var scalarNode = (YamlScalarNode)node; @@ -117,16 +123,15 @@ public IEnumerable Instances if (instanceType.IsGenericType && instanceType?.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { Type listType = instanceType?.GetGenericArguments()[0] ?? - throw new NullReferenceException($"The property {instanceType?.Name} of Class {parent?.GetType().Name} is not compatible with configuration"); + throw new NetDaemonNullReferenceException($"The property {instanceType?.Name} of Class {parent?.GetType().Name} is not compatible with configuration"); IList list = listType.CreateListOfPropertyType() ?? - throw new NullReferenceException("Failed to create listtype, plese check {prop.Name} of Class {app.GetType().Name}"); + throw new NetDaemonNullReferenceException("Failed to create listtype, plese check {prop.Name} of Class {app.GetType().Name}"); foreach (YamlNode item in ((YamlSequenceNode)node).Children) { - var instance = InstanceProperty(null, listType, item) ?? - throw new NotSupportedException($"The class {parent?.GetType().Name} has wrong type in items"); + throw new NotSupportedException($"The class {parent?.GetType().Name} has wrong type in items"); list.Add(instance); } @@ -162,7 +167,7 @@ public IEnumerable Instances break; case YamlNodeType.Mapping: - var map = (YamlMappingNode)entry.Value; + // Maps are not currently supported (var map = (YamlMappingNode)entry.Value;) break; } childProp.SetValue(instance, result); @@ -179,13 +184,13 @@ private void ReplaceSecretIfExists(YamlScalarNode scalarNode) var secretReplacement = _yamlConfig.GetSecretFromPath(scalarNode.Value!, Path.GetDirectoryName(_yamlFilePath)!); - scalarNode.Value = secretReplacement ?? throw new ApplicationException($"{scalarNode.Value!} not found in secrets.yaml"); + scalarNode.Value = secretReplacement ?? throw new NetDaemonException($"{scalarNode.Value!} not found in secrets.yaml"); } - private string? GetTypeNameFromClassConfig(YamlMappingNode appNode) + private static string? GetTypeNameFromClassConfig(YamlMappingNode appNode) { - KeyValuePair classChild = appNode.Children.Where(n => - ((YamlScalarNode)n.Key)?.Value?.ToLowerInvariant() == "class").FirstOrDefault(); + KeyValuePair classChild = appNode.Children.FirstOrDefault(n => + ((YamlScalarNode)n.Key)?.Value?.ToLowerInvariant() == "class"); if (classChild.Key == null || classChild.Value == null) { @@ -196,7 +201,8 @@ private void ReplaceSecretIfExists(YamlScalarNode scalarNode) { return null; } - return ((YamlScalarNode)classChild.Value)?.Value?.ToLowerInvariant(); + var scalarNode = (YamlScalarNode)classChild.Value; + return scalarNode.Value?.ToLowerInvariant(); } } } \ No newline at end of file diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlConfig.cs b/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlConfig.cs index 97855d261..538137a71 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlConfig.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlConfig.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; using Microsoft.Extensions.Options; using NetDaemon.Common.Configuration; +using NetDaemon.Common.Exceptions; using YamlDotNet.RepresentationModel; [assembly: InternalsVisibleTo("NetDaemon.Daemon.Tests")] @@ -23,7 +25,11 @@ public class YamlConfig : IYamlConfig public YamlConfig(IOptions netDaemonSettings) { + _ = netDaemonSettings ?? + throw new NetDaemonArgumentNullException(nameof(netDaemonSettings)); + _configFolder = netDaemonSettings.Value.GetAppSourceDirectory(); + _secrets = GetAllSecretsFromPath(_configFolder); } @@ -43,7 +49,7 @@ public IEnumerable GetAllConfigFilePaths() } if (configPath != _configFolder) { - var parentPath = Directory.GetParent(configPath)?.FullName ?? throw new ApplicationException("Parent folder of config path does not exist"); + var parentPath = Directory.GetParent(configPath)?.FullName ?? throw new NetDaemonException("Parent folder of config path does not exist"); return GetSecretFromPath(secret, parentPath); } @@ -52,9 +58,11 @@ public IEnumerable GetAllConfigFilePaths() internal static Dictionary GetSecretsFromSecretsYaml(string file) { - return GetSecretsFromSecretsYaml(File.OpenText(file)); + using var fileReader = File.OpenText(file); + return GetSecretsFromSecretsYaml(fileReader); } + [SuppressMessage("", "CA1508")] // TODO: Need to refactor this internal static Dictionary GetSecretsFromSecretsYaml(TextReader reader) { var result = new Dictionary(); @@ -93,8 +101,8 @@ public IEnumerable GetAllConfigFilePaths() if (!result.ContainsKey(fileDirectory)) { - var secretsFromFile = GetSecretsFromSecretsYaml(file); - result[fileDirectory] = secretsFromFile; + result[fileDirectory] = (Dictionary?)GetSecretsFromSecretsYaml(file) ?? + new Dictionary(); } } return result; diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlExtensions.cs b/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlExtensions.cs index 09b49f17a..0b13fe40f 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlExtensions.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlExtensions.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; +using NetDaemon.Common.Exceptions; using YamlDotNet.RepresentationModel; [assembly: InternalsVisibleTo("NetDaemon.Daemon.Tests")] @@ -25,19 +26,24 @@ public static class YamlExtensions { public static PropertyInfo? GetYamlProperty(this Type type, string propertyName) { - var prop = type.GetProperty(propertyName); + _ = type ?? + throw new NetDaemonArgumentNullException(nameof(type)); - if (prop == null) - { - // Lets try convert from python style to CamelCase - prop = type.GetProperty(propertyName.ToCamelCase()); - } + // Lets try convert from python style to CamelCase + + var prop = type.GetProperty(propertyName) ?? type.GetProperty(propertyName.ToCamelCase()); return prop; } public static object? ToObject(this YamlScalarNode node, Type valueType) { + _ = valueType ?? + throw new NetDaemonArgumentNullException(nameof(valueType)); + _ = node ?? + throw new NetDaemonArgumentNullException(nameof(node)); + Type? underlyingNullableType = Nullable.GetUnderlyingType(valueType); + if (underlyingNullableType != null) { // It is nullable type diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/DaemonAppExtensions.cs b/src/Daemon/NetDaemon.Daemon/Daemon/DaemonAppExtensions.cs index bde6b0c4d..3a0c0e36e 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/DaemonAppExtensions.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/DaemonAppExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Dynamic; using System.Linq; using System.Reflection; @@ -6,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NetDaemon.Common; +using NetDaemon.Common.Exceptions; using NetDaemon.Common.Fluent; using NetDaemon.Common.Reactive; using NetDaemon.Daemon.Config; @@ -16,9 +18,15 @@ namespace NetDaemon.Daemon { public static class DaemonAppExtensions { - public static async Task HandleAttributeInitialization(this INetDaemonAppBase netDaemonApp, INetDaemon _daemon) + public static async Task HandleAttributeInitialization(this INetDaemonAppBase netDaemonApp, INetDaemon daemon) { + _ = daemon ?? + throw new NetDaemonArgumentNullException(nameof(daemon)); + _ = netDaemonApp ?? + throw new NetDaemonArgumentNullException(nameof(netDaemonApp)); + var netDaemonAppType = netDaemonApp.GetType(); + foreach (var method in netDaemonAppType.GetMethods()) { foreach (var attr in method.GetCustomAttributes(false)) @@ -27,12 +35,12 @@ public static async Task HandleAttributeInitialization(this INetDaemonAppBase ne { switch (attr) { - case HomeAssistantServiceCallAttribute hasstServiceCallAttribute: - await HandleServiceCallAttribute(_daemon, daemonApp, method, true).ConfigureAwait(false); + case HomeAssistantServiceCallAttribute: + await HandleServiceCallAttribute(daemon, daemonApp, method, true).ConfigureAwait(false); break; case HomeAssistantStateChangedAttribute hassStateChangedAttribute: - HandleStateChangedAttribute(_daemon, hassStateChangedAttribute, daemonApp, method); + HandleStateChangedAttribute(daemon, hassStateChangedAttribute, daemonApp, method); break; } } @@ -40,8 +48,8 @@ public static async Task HandleAttributeInitialization(this INetDaemonAppBase ne { switch (attr) { - case HomeAssistantServiceCallAttribute hasstServiceCallAttribute: - await HandleServiceCallAttribute(_daemon, daemonRxApp, method, false).ConfigureAwait(false); + case HomeAssistantServiceCallAttribute: + await HandleServiceCallAttribute(daemon, daemonRxApp, method, false).ConfigureAwait(false); break; } } @@ -56,13 +64,15 @@ private static (bool, string) CheckIfServiceCallSignatureIsOk(MethodInfo method, var parameters = method.GetParameters(); - if (parameters == null || (parameters != null && parameters.Length != 1)) + if (parameters == null || parameters.Length != 1) return (false, $"{method.Name} has not correct number of parameters"); var dynParam = parameters![0]; if (dynParam.CustomAttributes.Count() == 1 && dynParam.CustomAttributes.First().AttributeType == typeof(DynamicAttribute)) + { return (true, string.Empty); + } return (false, $"{method.Name} is not correct signature"); } @@ -74,7 +84,7 @@ private static (bool, string) CheckIfStateChangedSignatureIsOk(MethodInfo method var parameters = method.GetParameters(); - if (parameters == null || (parameters != null && parameters.Length != 3)) + if (parameters == null || parameters.Length != 3) return (false, $"{method.Name} has not correct number of parameters"); if (parameters![0].ParameterType != typeof(string)) @@ -89,7 +99,8 @@ private static (bool, string) CheckIfStateChangedSignatureIsOk(MethodInfo method return (true, string.Empty); } - private static async Task HandleServiceCallAttribute(INetDaemon _daemon, NetDaemonAppBase netDaemonApp, MethodInfo method, bool async=true) + [SuppressMessage("", "CA1031")] + private static async Task HandleServiceCallAttribute(INetDaemon _daemon, NetDaemonAppBase netDaemonApp, MethodInfo method, bool async = true) { var (signatureOk, err) = CheckIfServiceCallSignatureIsOk(method, async); if (!signatureOk) @@ -118,8 +129,8 @@ private static async Task HandleServiceCallAttribute(INetDaemon _daemon, NetDaem } }); } - + [SuppressMessage("", "CA1031")] private static void HandleStateChangedAttribute( INetDaemon _daemon, HomeAssistantStateChangedAttribute hassStateChangedAttribute, @@ -140,13 +151,15 @@ MethodInfo method { try { - if (hassStateChangedAttribute.To != null) - if ((dynamic)hassStateChangedAttribute.To != to?.State) - return; + if (hassStateChangedAttribute.To != null && (dynamic)hassStateChangedAttribute.To != to?.State) + { + return; + } - if (hassStateChangedAttribute.From != null) - if ((dynamic)hassStateChangedAttribute.From != from?.State) - return; + if (hassStateChangedAttribute.From != null && (dynamic)hassStateChangedAttribute.From != from?.State) + { + return; + } // If we don´t accept all changes in the state change // and we do not have a state change so return diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/HttpHandler.cs b/src/Daemon/NetDaemon.Daemon/Daemon/HttpHandler.cs index 98a6a8a7c..1e0f01ecd 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/HttpHandler.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/HttpHandler.cs @@ -4,6 +4,7 @@ using System.Text.Json; using System.Threading.Tasks; using NetDaemon.Common; +using NetDaemon.Common.Exceptions; namespace NetDaemon.Daemon { @@ -18,70 +19,74 @@ public HttpHandler(IHttpClientFactory? httpClientFactory) public HttpClient CreateHttpClient(string? name = null) { - _ = _httpClientFactory ?? throw new NullReferenceException("No IHttpClientFactory provided, please add AddHttpClient() in configure services!"); + _ = _httpClientFactory ?? throw new NetDaemonNullReferenceException("No IHttpClientFactory provided, please add AddHttpClient() in configure services!"); return _httpClientFactory.CreateClient(name); } public async Task GetJson(string url, JsonSerializerOptions? options = null, params (string, object)[] headers) { - _ = _httpClientFactory ?? throw new NullReferenceException("No IHttpClientFactory provided, please add AddHttpClient() in configure services!"); + _ = _httpClientFactory ?? throw new NetDaemonNullReferenceException("No IHttpClientFactory provided, please add AddHttpClient() in configure services!"); - var httpClient = _httpClientFactory.CreateClient(); + using var httpClient = _httpClientFactory.CreateClient(); AddHeaders(httpClient, headers); - var streamTask = httpClient.GetStreamAsync(url) - ?? throw new ApplicationException($"Unexpected, nothing returned from {url}"); + var streamTask = httpClient.GetStreamAsync(new Uri(url)) + ?? throw new NetDaemonException($"Unexpected, nothing returned from {url}"); - return await JsonSerializer.DeserializeAsync(await streamTask.ConfigureAwait(false), options); + return await JsonSerializer.DeserializeAsync(await streamTask.ConfigureAwait(false), options).ConfigureAwait(false); } public async Task PostJson(string url, object request, JsonSerializerOptions? options = null, params (string, object)[] headers) { - _ = _httpClientFactory ?? throw new NullReferenceException("No IHttpClientFactory provided, please add AddHttpClient() in configure services!"); + _ = _httpClientFactory ?? throw new NetDaemonNullReferenceException("No IHttpClientFactory provided, please add AddHttpClient() in configure services!"); + _ = request ?? throw new NetDaemonArgumentNullException(nameof(request)); - var httpClient = _httpClientFactory.CreateClient(); + using var httpClient = _httpClientFactory.CreateClient(); AddHeaders(httpClient, headers); var bytesToPost = JsonSerializer.SerializeToUtf8Bytes(request, request.GetType(), options); + using var content = new ByteArrayContent(bytesToPost); - var response = await httpClient.PostAsync(url, new ByteArrayContent(bytesToPost)); + var response = await httpClient.PostAsync(new Uri(url), content).ConfigureAwait(false); response.EnsureSuccessStatusCode(); var streamTask = response.Content.ReadAsStreamAsync(); - return await JsonSerializer.DeserializeAsync(await streamTask.ConfigureAwait(false)); + return await JsonSerializer.DeserializeAsync(await streamTask.ConfigureAwait(false)).ConfigureAwait(false); } public async Task PostJson(string url, object request, JsonSerializerOptions? options = null, params (string, object)[] headers) { - _ = _httpClientFactory ?? throw new NullReferenceException("No IHttpClientFactory provided, please add AddHttpClient() in configure services!"); + _ = _httpClientFactory ?? throw new NetDaemonNullReferenceException("No IHttpClientFactory provided, please add AddHttpClient() in configure services!"); + _ = request ?? throw new NetDaemonArgumentNullException(nameof(request)); - var httpClient = _httpClientFactory.CreateClient(); + using var httpClient = _httpClientFactory.CreateClient(); AddHeaders(httpClient, headers); var bytesToPost = JsonSerializer.SerializeToUtf8Bytes(request, request.GetType(), options); + using var content = new ByteArrayContent(bytesToPost); - var response = await httpClient.PostAsync(url, new ByteArrayContent(bytesToPost)); + var response = await httpClient.PostAsync(new Uri(url), content).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } - private void AddHeaders(HttpClient httpClient, (string, object)[] headers) + private static void AddHeaders(HttpClient httpClient, (string, object)[] headers) { if (headers is not null && headers.Length > 0) { httpClient.DefaultRequestHeaders.Clear(); foreach (var (name, header) in headers) { - if (header is string) - httpClient.DefaultRequestHeaders.Add(name, (string)header); - else if (header is IEnumerable) - httpClient.DefaultRequestHeaders.Add(name, (IEnumerable)header); + if (header is string headerStr) + httpClient.DefaultRequestHeaders.Add(name, headerStr); + else if (header is IEnumerable headerStrings) + httpClient.DefaultRequestHeaders.Add(name, headerStrings); else - throw new ApplicationException($"Unsupported header, expected string or IEnumerable for {name}"); + throw new NetDaemonException($"Unsupported header, expected string or IEnumerable for {name}"); } } } diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/IInstanceDaemonApp.cs b/src/Daemon/NetDaemon.Daemon/Daemon/IInstanceDaemonApp.cs index 4c8271453..0de6fc692 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/IInstanceDaemonApp.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/IInstanceDaemonApp.cs @@ -19,7 +19,6 @@ public interface IInstanceDaemonApp /// /// Returns a list of instanced daemonapps /// - /// IEnumerable InstanceDaemonApps(); } } \ No newline at end of file diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/INetDaemonHost.cs b/src/Daemon/NetDaemon.Daemon/Daemon/INetDaemonHost.cs index 09b8cf9aa..52a61f27f 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/INetDaemonHost.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/INetDaemonHost.cs @@ -8,7 +8,6 @@ namespace NetDaemon.Daemon { - /// /// The interface that interacts with the daemon host main logic /// diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs b/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs index e23f0b119..b08716288 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Dynamic; +using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -11,6 +13,7 @@ using JoySoftware.HomeAssistant.Client; using Microsoft.Extensions.Logging; using NetDaemon.Common; +using NetDaemon.Common.Exceptions; using NetDaemon.Common.Fluent; using NetDaemon.Common.Reactive; using NetDaemon.Daemon.Storage; @@ -24,17 +27,18 @@ namespace NetDaemon.Daemon { public class NetDaemonHost : INetDaemonHost, IAsyncDisposable { + private static readonly ConcurrentBag> concurrentBag = new(); internal readonly ConcurrentBag> _externalEventCallSubscribers = - new ConcurrentBag>(); + concurrentBag; internal readonly ConcurrentDictionary _hassAreas = - new ConcurrentDictionary(); + new(); // Internal for test internal readonly ConcurrentDictionary _hassDevices = - new ConcurrentDictionary(); + new(); internal readonly ConcurrentDictionary _hassEntities = - new ConcurrentDictionary(); + new(); internal readonly Channel<(string, string, dynamic?)> _serviceCallMessageChannel = Channel.CreateBounded<(string, string, dynamic?)>(200); @@ -54,10 +58,10 @@ public class NetDaemonHost : INetDaemonHost, IAsyncDisposable private IInstanceDaemonApp? _appInstanceManager; // Internal token source for just cancel this objects activities - private readonly CancellationTokenSource _cancelDaemon = new CancellationTokenSource(); + private readonly CancellationTokenSource _cancelDaemon = new(); private readonly ConcurrentBag<(string, string, Func)> _daemonServiceCallFunctions - = new ConcurrentBag<(string, string, Func)>(); + = new(); /// /// Currently running tasks for handling new events from HomeAssistant @@ -70,36 +74,18 @@ public class NetDaemonHost : INetDaemonHost, IAsyncDisposable private readonly IDataRepository? _repository; - private readonly ConcurrentDictionary _runningAppInstances = - new ConcurrentDictionary(); - private readonly ConcurrentDictionary _allAppInstances = - new ConcurrentDictionary(); - /// /// Used for testing /// - internal ConcurrentDictionary InternalAllAppInstances => _allAppInstances; + internal ConcurrentDictionary InternalAllAppInstances { get; } = new(); private readonly Scheduler _scheduler; - - private readonly List _supportedDomainsForTurnOnOff = new() - { - "light", - "switch", - "input_boolean", - "automation", - "input_boolean", - "camera", - "scene", - "script", - }; + private bool _isDisposed; // Following token source and token are set at RUN private CancellationToken _cancelToken; private CancellationTokenSource? _cancelTokenSource; - private IDictionary _dataCache = new Dictionary(); - /// /// Constructor @@ -108,7 +94,6 @@ public class NetDaemonHost : INetDaemonHost, IAsyncDisposable /// Repository to use /// The loggerfactory /// Http handler to use - /// Handles instances of apps public NetDaemonHost( IHassClient? hassClient, IDataRepository? repository, @@ -119,9 +104,11 @@ public class NetDaemonHost : INetDaemonHost, IAsyncDisposable loggerFactory ??= DefaultLoggerFactory; _httpHandler = httpHandler; Logger = loggerFactory.CreateLogger(); - _hassClient = hassClient ?? throw new ArgumentNullException("HassClient can't be null!"); + _hassClient = hassClient + ?? throw new ArgumentNullException(nameof(hassClient)); _scheduler = new Scheduler(loggerFactory: loggerFactory); _repository = repository; + _isDisposed = false; Logger.LogTrace("Instance NetDaemonHost"); } @@ -131,23 +118,24 @@ public IHttpHandler Http { get { - _ = _httpHandler ?? throw new NullReferenceException("HttpHandler can not be null!"); + _ = _httpHandler ?? throw new NetDaemonNullReferenceException("HttpHandler can not be null!"); return _httpHandler; } } public ILogger Logger { get; } - public IEnumerable RunningAppInstances => _runningAppInstances.Values; + public IEnumerable RunningAppInstances => InternalRunningAppInstances.Values; - public IEnumerable AllAppInstances => _allAppInstances.Values; + public IEnumerable AllAppInstances => InternalAllAppInstances.Values; public IScheduler Scheduler => _scheduler; + [SuppressMessage("", "CA1721")] public IEnumerable State => InternalState.Select(n => n.Value); // For testing - internal ConcurrentDictionary InternalRunningAppInstances => _runningAppInstances; + internal ConcurrentDictionary InternalRunningAppInstances { get; } = new(); private static ILoggerFactory DefaultLoggerFactory => LoggerFactory.Create(builder => { @@ -156,25 +144,27 @@ public IHttpHandler Http .AddConsole(); }); + public IDictionary DataCache { get; } = new Dictionary(); public async Task> GetAllServices() { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); - return await _hassClient.GetServices(); + return await _hassClient.GetServices().ConfigureAwait(false); } public void CallService(string domain, string service, dynamic? data = null) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); - if (_serviceCallMessageChannel.Writer.TryWrite((domain, service, data)) == false) - throw new ApplicationException("Servicecall queue full!"); + if (!_serviceCallMessageChannel.Writer.TryWrite((domain, service, data))) + throw new NetDaemonException("Servicecall queue full!"); } + [SuppressMessage("", "CA1031")] public async Task CallServiceAsync(string domain, string service, dynamic? data = null, bool waitForResponse = false) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); try { @@ -189,21 +179,23 @@ public async Task CallServiceAsync(string domain, string service, dynamic? data /// public ICamera Camera(INetDaemonApp app, params string[] entityIds) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); return new CameraManager(entityIds, this, app); } /// public ICamera Cameras(INetDaemonApp app, IEnumerable entityIds) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); return new CameraManager(entityIds, this, app); } /// public ICamera Cameras(INetDaemonApp app, Func func) { - this._cancelToken.ThrowIfCancellationRequested(); + _ = app ?? + throw new NetDaemonArgumentNullException(nameof(app)); + _cancelToken.ThrowIfCancellationRequested(); try { IEnumerable x = State.Where(func); @@ -219,22 +211,35 @@ public ICamera Cameras(INetDaemonApp app, Func func) public async ValueTask DisposeAsync() { + lock (_cancelDaemon) + { + if (_isDisposed) + return; + _isDisposed = true; + } + _cancelDaemon.Cancel(); await Stop().ConfigureAwait(false); + await _scheduler.DisposeAsync().ConfigureAwait(false); + _cancelDaemon.Dispose(); + _cancelTokenSource?.Dispose(); + Logger.LogTrace("Instance NetDaemonHost Disposed"); } - public void EnableApplicationDiscoveryServiceAsync() + public void EnableApplicationDiscoveryService() { // For service call reload_apps we do just that... reload the fucking apps yay :) - ListenCompanionServiceCall("reload_apps", async (_) => await ReloadAllApps()); + ListenCompanionServiceCall("reload_apps", async (_) => await ReloadAllApps().ConfigureAwait(false)); RegisterAppSwitchesAndTheirStates(); } public IEntity Entities(INetDaemonApp app, Func func) { - this._cancelToken.ThrowIfCancellationRequested(); + _ = app ?? + throw new NetDaemonArgumentNullException(nameof(app)); + _cancelToken.ThrowIfCancellationRequested(); try { @@ -249,51 +254,52 @@ public IEntity Entities(INetDaemonApp app, Func func) } } - public IEntity Entities(INetDaemonApp app, IEnumerable entityIds) + public IEntity Entities(INetDaemonApp app, IEnumerable entityId) { - this._cancelToken.ThrowIfCancellationRequested(); - return new EntityManager(entityIds, this, app); + _cancelToken.ThrowIfCancellationRequested(); + return new EntityManager(entityId, this, app); } - public IEntity Entity(INetDaemonApp app, params string[] entityIds) + public IEntity Entity(INetDaemonApp app, params string[] entityId) { - this._cancelToken.ThrowIfCancellationRequested(); - return new EntityManager(entityIds, this, app); + _cancelToken.ThrowIfCancellationRequested(); + return new EntityManager(entityId, this, app); } /// public INetDaemonAppBase? GetApp(string appInstanceId) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); - return _runningAppInstances.ContainsKey(appInstanceId) ? - _runningAppInstances[appInstanceId] : null; + return InternalRunningAppInstances.ContainsKey(appInstanceId) ? + InternalRunningAppInstances[appInstanceId] : null; } - public async ValueTask GetDataAsync(string id) where T : class + public async Task GetDataAsync(string id) where T : class { - this._cancelToken.ThrowIfCancellationRequested(); - - _ = _repository as IDataRepository ?? - throw new NullReferenceException($"{nameof(_repository)} can not be null!"); + _cancelToken.ThrowIfCancellationRequested(); + _ = _repository ?? + throw new NetDaemonNullReferenceException($"{nameof(_repository)} can not be null!"); - if (_dataCache.ContainsKey(id)) + if (DataCache.ContainsKey(id)) { - return (T)_dataCache[id]; + return (T)DataCache[id]; } var data = await _repository!.Get(id).ConfigureAwait(false); if (data != null) - _dataCache[id] = data; + DataCache[id] = data; return data; } - public EntityState? GetState(string entity) + public EntityState? GetState(string entityId) { - this._cancelToken.ThrowIfCancellationRequested(); + _ = entityId ?? + throw new NetDaemonArgumentNullException(nameof(entityId)); + _cancelToken.ThrowIfCancellationRequested(); - return InternalState.TryGetValue(entity, out EntityState? returnValue) + return InternalState.TryGetValue(entityId, out EntityState? returnValue) ? returnValue : null; } @@ -302,32 +308,32 @@ public IEntity Entity(INetDaemonApp app, params string[] entityIds) public async Task Initialize(IInstanceDaemonApp appInstanceManager) { if (!Connected) - throw new ApplicationException("NetDaemon is not connected, no use in initializing"); + throw new NetDaemonException("NetDaemon is not connected, no use in initializing"); _appInstanceManager = appInstanceManager; await LoadAllApps().ConfigureAwait(false); - EnableApplicationDiscoveryServiceAsync(); + EnableApplicationDiscoveryService(); } /// public IFluentInputSelect InputSelect(INetDaemonApp app, params string[] inputSelectParams) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); return new InputSelectManager(inputSelectParams, this, app); } /// public IFluentInputSelect InputSelects(INetDaemonApp app, IEnumerable inputSelectParams) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); return new InputSelectManager(inputSelectParams, this, app); } /// public IFluentInputSelect InputSelects(INetDaemonApp app, Func func) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); IEnumerable x = State.Where(func).Select(n => n.EntityId); return new InputSelectManager(x, this, app); } @@ -335,31 +341,41 @@ public IFluentInputSelect InputSelects(INetDaemonApp app, Func public void ListenCompanionServiceCall(string service, Func action) { - this._cancelToken.ThrowIfCancellationRequested(); + _ = service ?? + throw new NetDaemonArgumentNullException(nameof(service)); + _cancelToken.ThrowIfCancellationRequested(); _daemonServiceCallFunctions.Add(("netdaemon", service.ToLowerInvariant(), action)); } public void ListenServiceCall(string domain, string service, Func action) - => _daemonServiceCallFunctions.Add((domain.ToLowerInvariant(), service.ToLowerInvariant(), action)); + { + _ = service ?? + throw new NetDaemonArgumentNullException(nameof(service)); + _ = domain ?? + throw new NetDaemonArgumentNullException(nameof(domain)); + _daemonServiceCallFunctions.Add((domain.ToLowerInvariant(), service.ToLowerInvariant(), action)); + } /// public IMediaPlayer MediaPlayer(INetDaemonApp app, params string[] entityIds) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); return new MediaPlayerManager(entityIds, this, app); } /// public IMediaPlayer MediaPlayers(INetDaemonApp app, IEnumerable entityIds) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); return new MediaPlayerManager(entityIds, this, app); } /// public IMediaPlayer MediaPlayers(INetDaemonApp app, Func func) { - this._cancelToken.ThrowIfCancellationRequested(); + _ = app ?? + throw new NetDaemonArgumentNullException(nameof(app)); + _cancelToken.ThrowIfCancellationRequested(); try { IEnumerable x = State.Where(func); @@ -393,6 +409,7 @@ public async Task ReloadAllApps() /// /// /// + [SuppressMessage("", "CA1031")] public async Task Run(string host, short port, bool ssl, string token, CancellationToken cancellationToken) { // Create combine cancellation token @@ -405,7 +422,7 @@ public async Task Run(string host, short port, bool ssl, string token, Cancellat if (_hassClient == null) { - throw new NullReferenceException("HassClient cant be null when running daemon, check constructor!"); + throw new NetDaemonNullReferenceException("HassClient cant be null when running daemon, check constructor!"); } try @@ -434,14 +451,14 @@ public async Task Run(string host, short port, bool ssl, string token, Cancellat if (hassConfig.State != "RUNNING") { Logger.LogInformation("Home Assistant is not ready yet, state: {state} ..", hassConfig.State); - await _hassClient.CloseAsync(); + await _hassClient.CloseAsync().ConfigureAwait(false); return; } // Setup TTS Task handleTextToSpeechMessagesTask = HandleTextToSpeechMessages(cancellationToken); Task handleAsyncServiceCalls = HandleAsyncServiceCalls(cancellationToken); - Task hanldeAsyncSetState = HandleAsyncSetState(cancellationToken); + Task handleAsyncSetState = HandleAsyncSetState(cancellationToken); await RefreshInternalStatesAndSetArea().ConfigureAwait(false); @@ -459,16 +476,13 @@ public async Task Run(string host, short port, bool ssl, string token, Cancellat HassEvent changedEvent = await _hassClient.ReadEventAsync(cancellationToken).ConfigureAwait(false); if (changedEvent != null) { - if (changedEvent.Data is HassServiceEventData hseData) - { - if (hseData.Domain == "homeassistant" && + if (changedEvent.Data is HassServiceEventData hseData && hseData.Domain == "homeassistant" && (hseData.Service == "stop" || hseData.Service == "restart")) - { - // The user stopped HA so just stop processing messages - Logger.LogInformation("User {action} Home Assistant, will try to reconnect...", - hseData.Service == "stop" ? "stopping" : "restarting"); - return; - } + { + // The user stopped HA so just stop processing messages + Logger.LogInformation("User {action} Home Assistant, will try to reconnect...", + hseData.Service == "stop" ? "stopping" : "restarting"); + return; } // Remove all completed Tasks _eventHandlerTasks.RemoveAll(x => x.IsCompleted); @@ -499,29 +513,30 @@ public async Task Run(string host, short port, bool ssl, string token, Cancellat } } - public IScript RunScript(INetDaemonApp app, params string[] entityId) + public IScript RunScript(INetDaemonApp app, params string[] entityIds) { - this._cancelToken.ThrowIfCancellationRequested(); - return new EntityManager(entityId, this, app); + _cancelToken.ThrowIfCancellationRequested(); + return new EntityManager(entityIds, this, app); } public Task SaveDataAsync(string id, T data) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); - _ = _repository as IDataRepository ?? - throw new NullReferenceException($"{nameof(_repository)} can not be null!"); + _ = _repository ?? + throw new NetDaemonNullReferenceException($"{nameof(_repository)} can not be null!"); if (data == null) - throw new ArgumentNullException(nameof(data)); + throw new NetDaemonArgumentNullException(nameof(data)); - _dataCache[id] = data; + DataCache[id] = data; return _repository!.Save(id, data); } + [SuppressMessage("", "CA1031")] public async Task SendEvent(string eventId, dynamic? data = null) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); if (!Connected) return false; @@ -539,11 +554,11 @@ public async Task SendEvent(string eventId, dynamic? data = null) /// public async Task SetDaemonStateAsync(int numberOfLoadedApps, int numberOfRunningApps) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); await SetStateAsync( "netdaemon.status", - "Connected", // State will alawys be connected, otherwise state could not be set. + "Connected", // State will always be connected, otherwise state could not be set. ("number_of_loaded_apps", numberOfLoadedApps), ("number_of_running_apps", numberOfRunningApps), ("version", GetType().Assembly.GetName().Version?.ToString() ?? "N/A")).ConfigureAwait(false); @@ -551,16 +566,16 @@ public async Task SetDaemonStateAsync(int numberOfLoadedApps, int numberOfRunnin public void SetState(string entityId, dynamic state, dynamic? attributes = null) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); - if (_setStateMessageChannel.Writer.TryWrite((entityId, state, attributes)) == false) - throw new ApplicationException("Servicecall queue full!"); + if (!_setStateMessageChannel.Writer.TryWrite((entityId, state, attributes))) + throw new NetDaemonException("Servicecall queue full!"); } public async Task SetStateAsync(string entityId, dynamic state, params (string name, object val)[] attributes) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); try { // Use expando object as all other methods @@ -591,11 +606,12 @@ public void SetState(string entityId, dynamic state, dynamic? attributes = null) public void Speak(string entityId, string message) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); _ttsMessageChannel.Writer.TryWrite((entityId, message)); } + [SuppressMessage("", "CA1031")] public async Task Stop() { try @@ -606,9 +622,15 @@ public async Task Stop() await _scheduler.Stop().ConfigureAwait(false); - InternalState.Clear(); - + InternalAllAppInstances.Clear(); + InternalRunningAppInstances.Clear(); + _hassAreas.Clear(); + _hassDevices.Clear(); + _hassEntities.Clear(); + _daemonServiceCallFunctions.Clear(); + _externalEventCallSubscribers.Clear(); + _eventHandlerTasks.Clear(); Connected = false; await _hassClient.CloseAsync().ConfigureAwait(false); @@ -622,14 +644,23 @@ public async Task Stop() } /// + [SuppressMessage("", "CA1031")] public async Task UnloadAllApps() { - foreach (var app in _allAppInstances) + Logger.LogTrace("Unloading all apps ({instances}, {running})", InternalAllAppInstances.Count, InternalRunningAppInstances.Count); + foreach (var app in InternalAllAppInstances) { - await app.Value.DisposeAsync().ConfigureAwait(false); + try + { + await app.Value.DisposeAsync().ConfigureAwait(false); + } + catch (Exception e) + { + Logger.LogError(e, "Failed to unload apps, {app_id}", app.Value.Id); + } } - _allAppInstances.Clear(); - _runningAppInstances.Clear(); + InternalAllAppInstances.Clear(); + InternalRunningAppInstances.Clear(); } /// @@ -638,7 +669,7 @@ public async Task UnloadAllApps() /// /// The state data to be fixed /// - /// If a sensor is unavailable that normally has a primtive value + /// If a sensor is unavailable that normally has a primitive value /// it can be a string. The automations might expect a integer. /// Another scenario is that a value of 10 is cast as long and /// next time a value of 11.3 is cast to double. T @@ -704,24 +735,15 @@ internal static bool FixStateTypes(HassStateChangedEventData stateData) internal string? GetAreaForEntityId(string entityId) { - HassEntity? entity; - if (_hassEntities.TryGetValue(entityId, out entity) && entity is not null) + if (_hassEntities.TryGetValue(entityId, out HassEntity? entity) && entity is not null && entity.DeviceId is not null) { - if (entity.DeviceId is not null) + // The entity is on a device + if (_hassDevices.TryGetValue(entity.DeviceId, out HassDevice? device) && device is not null && device.AreaId is not null) { - // The entity is on a device - HassDevice? device; - if (_hassDevices.TryGetValue(entity.DeviceId, out device) && device is not null) + // This device is in an area + if (_hassAreas.TryGetValue(device.AreaId, out HassArea? area) && area is not null) { - if (device.AreaId is not null) - { - // This device is in an area - HassArea? area; - if (_hassAreas.TryGetValue(device.AreaId, out area) && area is not null) - { - return area.Name; - } - } + return area.Name; } } } @@ -730,7 +752,7 @@ internal static bool FixStateTypes(HassStateChangedEventData stateData) internal async Task RefreshInternalStatesAndSetArea() { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); foreach (var device in await _hassClient.GetDevices().ConfigureAwait(false)) { @@ -763,35 +785,38 @@ internal async Task RefreshInternalStatesAndSetArea() } } - internal IList SortByDependency(IEnumerable unsortedList) + internal static IList SortByDependency(IEnumerable unsortedList) { - if (unsortedList.SelectMany(n => n.Dependencies).Count() > 0) + if (unsortedList.SelectMany(n => n.Dependencies).Any()) { - // There are dependecies defined + // There are dependencies defined var edges = new HashSet>(); foreach (var instance in unsortedList) { foreach (var dependency in instance.Dependencies) { - var dependentApp = unsortedList.Where(n => n.Id == dependency).FirstOrDefault(); + var dependentApp = unsortedList.FirstOrDefault(n => n.Id == dependency); if (dependentApp == null) - throw new ApplicationException($"There is no app named {dependency}, please check dependencies or make sure you have not disabled the dependent app!"); + throw new NetDaemonException($"There is no app named {dependency}, please check dependencies or make sure you have not disabled the dependent app!"); edges.Add(new Tuple(instance, dependentApp)); } } var sortedInstances = TopologicalSort(unsortedList.ToHashSet(), edges) ?? - throw new ApplicationException("Application dependencies is wrong, please check dependencies for circular dependencies!"); + throw new NetDaemonException("Application dependencies is wrong, please check dependencies for circular dependencies!"); return sortedInstances; } return unsortedList.ToList(); } + [SuppressMessage("", "CA1031")] protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationToken token) { _cancelToken.ThrowIfCancellationRequested(); + _ = hassEvent ?? + throw new NetDaemonArgumentNullException(nameof(hassEvent)); if (hassEvent.EventType == "state_changed") { @@ -801,7 +826,7 @@ protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationTok if (stateData is null) { - throw new NullReferenceException("StateData is null!"); + throw new NetDaemonNullReferenceException("StateData is null!"); } if (stateData.NewState is null || stateData.OldState is null) @@ -812,16 +837,16 @@ protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationTok if (!FixStateTypes(stateData)) { - if (stateData?.NewState?.State != stateData?.OldState?.State) + if (stateData.NewState?.State != stateData.OldState?.State) { var sb = new StringBuilder(); - sb.AppendLine($"Can not fix state typing for {stateData?.NewState?.EntityId}"); - sb.AppendLine($"NewStateObject: {stateData?.NewState}"); - sb.AppendLine($"OldStateObject: {stateData?.OldState}"); - sb.AppendLine($"NewState: {stateData?.NewState?.State}"); - sb.AppendLine($"OldState: {stateData?.OldState?.State}"); - sb.AppendLine($"NewState type: {stateData?.NewState?.State?.GetType().ToString() ?? "null"}"); - sb.AppendLine($"OldState type: {stateData?.OldState?.State?.GetType().ToString() ?? "null"}"); + sb.Append("Can not fix state typing for ").AppendLine(stateData.NewState?.EntityId); + sb.Append("NewStateObject: ").Append(stateData.NewState).AppendLine(); + sb.Append("OldStateObject: ").Append(stateData.OldState).AppendLine(); + sb.Append("NewState: ").AppendLine(stateData.NewState?.State); + sb.Append("OldState: ").AppendLine(stateData.OldState?.State); + sb.Append("NewState type: ").AppendLine(stateData.NewState?.State?.GetType().ToString() ?? "null"); + sb.Append("OldState type: ").AppendLine(stateData.OldState?.State?.GetType().ToString() ?? "null"); Logger.LogTrace(sb.ToString()); } return; @@ -835,7 +860,7 @@ protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationTok InternalState[stateData.EntityId] = newState; var tasks = new List(); - foreach (var app in _runningAppInstances) + foreach (var app in InternalRunningAppInstances) { if (app.Value is NetDaemonApp netDaemonApp) { @@ -848,7 +873,7 @@ protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationTok oldState )); } - else if (stateData.EntityId.StartsWith(pattern)) + else if (stateData.EntityId.StartsWith(pattern, StringComparison.InvariantCultureIgnoreCase)) { tasks.Add(func(stateData.EntityId, newState, @@ -873,7 +898,7 @@ protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationTok observer.OnError(e); netDaemonRxApp.LogError(e, $"Fail to OnNext on state change observer. {newState.EntityId}:{newState?.State}({oldState?.State})"); } - })); + }, token)); } } } @@ -882,7 +907,6 @@ protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationTok // Todo: Make it timeout! Maybe it should be handling in it's own task like scheduler if (tasks.Count > 0) { - await tasks.WhenAll(token).ConfigureAwait(false); await tasks.WhenAll(token).ConfigureAwait(false); @@ -901,10 +925,10 @@ protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationTok if (serviceCallData == null) { - throw new NullReferenceException("ServiceData is null! not expected"); + throw new NetDaemonNullReferenceException("ServiceData is null! not expected"); } var tasks = new List(); - foreach (var app in _runningAppInstances) + foreach (var app in InternalRunningAppInstances) { // Call any service call registered if (app.Value is NetDaemonApp netDaemonApp) @@ -952,7 +976,7 @@ protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationTok } Logger.LogError(e, "Fail to OnNext on event observer (service_call)"); } - })); + }, token)); } } } @@ -999,7 +1023,7 @@ protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationTok hassEvent.Data = new FluentExpandoObject(true, true, exObject); } var tasks = new List(); - foreach (var app in _runningAppInstances) + foreach (var app in InternalRunningAppInstances) { if (app.Value is NetDaemonApp netDaemonApp) { @@ -1035,7 +1059,7 @@ protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationTok observer.OnError(e); Logger.LogError(e, "Fail to OnNext on event observer (event)"); } - })); + }, token)); } } } @@ -1051,17 +1075,6 @@ protected virtual async Task HandleNewEvent(HassEvent hassEvent, CancellationTok } } - private static string GetDomainFromEntity(string entity) - { - string[] entityParts = entity.Split('.'); - if (entityParts.Length != 2) - { - throw new ApplicationException($"entity_id is mal formatted {entity}"); - } - - return entityParts[0]; - } - /// /// Topological Sorting (Kahn's algorithm) /// @@ -1076,10 +1089,10 @@ private static string GetDomainFromEntity(string entity) var L = new List(); // Set of all nodes with no incoming edges - var S = new HashSet(nodes.Where(n => edges.All(e => e.Item2.Equals(n) == false))); + var S = new HashSet(nodes.Where(n => edges.All(e => !e.Item2.Equals(n)))); // while S is non-empty do - while (S.Any()) + while (S.Count > 0) { // remove a node n from S var n = S.First(); @@ -1097,7 +1110,7 @@ private static string GetDomainFromEntity(string entity) edges.Remove(e); // if m has no other incoming edges then - if (edges.All(me => me.Item2.Equals(m) == false)) + if (edges.All(me => !me.Item2.Equals(m))) { // insert m into S S.Add(m); @@ -1106,7 +1119,7 @@ private static string GetDomainFromEntity(string entity) } // if graph has edges then - if (edges.Any()) + if (edges.Count > 0) { // return error (graph has at least one cycle) return null; @@ -1118,10 +1131,10 @@ private static string GetDomainFromEntity(string entity) return L; } } - + [SuppressMessage("", "CA1031")] private async Task HandleAsyncServiceCalls(CancellationToken cancellationToken) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); bool hasLoggedError = false; @@ -1133,7 +1146,7 @@ private async Task HandleAsyncServiceCalls(CancellationToken cancellationToken) (string domain, string service, dynamic? data) = await _serviceCallMessageChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); - await _hassClient.CallService(domain, service, data, false).ConfigureAwait(false); ; + await _hassClient.CallService(domain, service, data, false).ConfigureAwait(false); hasLoggedError = false; } @@ -1143,17 +1156,18 @@ private async Task HandleAsyncServiceCalls(CancellationToken cancellationToken) } catch (Exception e) { - if (hasLoggedError == false) + if (!hasLoggedError) Logger.LogDebug(e, "Failure sending call service"); hasLoggedError = true; - await Task.Delay(100); // Do a delay to avoid loop + await Task.Delay(100, cancellationToken).ConfigureAwait(false); // Do a delay to avoid loop } } } + [SuppressMessage("", "CA1031")] private async Task HandleAsyncSetState(CancellationToken cancellationToken) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); bool hasLoggedError = false; //_serviceCallMessageQueue @@ -1174,17 +1188,18 @@ private async Task HandleAsyncSetState(CancellationToken cancellationToken) } catch (Exception e) { - if (hasLoggedError == false) + if (!hasLoggedError) Logger.LogDebug(e, "Failure setting state"); hasLoggedError = true; - await Task.Delay(100); // Do a delay to avoid loop + await Task.Delay(100, cancellationToken).ConfigureAwait(false); // Do a delay to avoid loop } } } + [SuppressMessage("", "CA1031")] private async Task HandleTextToSpeechMessages(CancellationToken cancellationToken) { - this._cancelToken.ThrowIfCancellationRequested(); + _cancelToken.ThrowIfCancellationRequested(); try { while (!cancellationToken.IsCancellationRequested) @@ -1195,17 +1210,17 @@ private async Task HandleTextToSpeechMessages(CancellationToken cancellationToke attributes.entity_id = entityId; attributes.message = message; await _hassClient.CallService("tts", "google_cloud_say", attributes, true).ConfigureAwait(false); - await Task.Delay(InternalDelayTimeForTts).ConfigureAwait(false); // Wait 2 seconds to wait for status to complete + await Task.Delay(InternalDelayTimeForTts, cancellationToken).ConfigureAwait(false); // Wait 2 seconds to wait for status to complete EntityState? currentPlayState = GetState(entityId); - if (currentPlayState != null && currentPlayState.Attribute?.media_duration != null) + if (currentPlayState?.Attribute?.media_duration != null) { int delayInMilliSeconds = (int)Math.Round(currentPlayState?.Attribute?.media_duration * 1000) - InternalDelayTimeForTts; if (delayInMilliSeconds > 0) { - await Task.Delay(delayInMilliSeconds).ConfigureAwait(false); // Wait remainder of text message + await Task.Delay(delayInMilliSeconds, cancellationToken).ConfigureAwait(false); // Wait remainder of text message } } } @@ -1223,29 +1238,31 @@ private async Task HandleTextToSpeechMessages(CancellationToken cancellationToke /// private async Task LoadAllApps() { - _ = _appInstanceManager ?? throw new NullReferenceException(nameof(_appInstanceManager)); - + _ = _appInstanceManager ?? throw new NetDaemonNullReferenceException(nameof(_appInstanceManager)); + Logger.LogTrace("Loading all apps ({instances}, {running})", InternalAllAppInstances.Count, InternalRunningAppInstances.Count); // First unload any apps running await UnloadAllApps().ConfigureAwait(false); // Get all instances var instancedApps = _appInstanceManager.InstanceDaemonApps(); - if (_runningAppInstances.Count() > 0) - throw new ApplicationException("Did not expect running instances!"); + if (!InternalRunningAppInstances.IsEmpty) + { + Logger.LogWarning("Old apps not unloaded correctly. {nr} apps still loaded.", InternalRunningAppInstances.Count); + InternalRunningAppInstances.Clear(); + } foreach (INetDaemonAppBase appInstance in instancedApps!) { - _allAppInstances[appInstance.Id!] = appInstance; + InternalAllAppInstances[appInstance.Id!] = appInstance; if (await RestoreAppState(appInstance).ConfigureAwait(false)) { - _runningAppInstances[appInstance.Id!] = appInstance; + InternalRunningAppInstances[appInstance.Id!] = appInstance; } - } // Now run initialize on all sorted by dependencies - foreach (var sortedApp in SortByDependency(_runningAppInstances.Values)) + foreach (var sortedApp in SortByDependency(InternalRunningAppInstances.Values)) { // Init by calling the InitializeAsync var taskInitAsync = sortedApp.InitializeAsync(); @@ -1260,24 +1277,19 @@ private async Task LoadAllApps() Logger.LogWarning("Initialize of application {app} took longer that 5 seconds, make sure Initialize function is not blocking!", sortedApp.Id); // Todo: refactor - await sortedApp.HandleAttributeInitialization(this); + await sortedApp.HandleAttributeInitialization(this).ConfigureAwait(false); Logger.LogInformation("Successfully loaded app {appId} ({class})", sortedApp.Id, sortedApp.GetType().Name); } - await SetDaemonStateAsync(_appInstanceManager.Count, _runningAppInstances.Count).ConfigureAwait(false); + await SetDaemonStateAsync(_appInstanceManager.Count, InternalRunningAppInstances.Count).ConfigureAwait(false); } + [SuppressMessage("", "CA1031")] private void RegisterAppSwitchesAndTheirStates() { - ListenServiceCall("switch", "turn_on", async (data) => - { - await SetStateOnDaemonAppSwitch("on", data).ConfigureAwait(false); - }); + ListenServiceCall("switch", "turn_on", async (data) => await SetStateOnDaemonAppSwitch("on", data).ConfigureAwait(false)); - ListenServiceCall("switch", "turn_off", async (data) => - { - await SetStateOnDaemonAppSwitch("off", data).ConfigureAwait(false); - }); + ListenServiceCall("switch", "turn_off", async (data) => await SetStateOnDaemonAppSwitch("off", data).ConfigureAwait(false)); ListenServiceCall("switch", "toggle", async (data) => { @@ -1294,7 +1306,7 @@ private void RegisterAppSwitchesAndTheirStates() else await SetStateOnDaemonAppSwitch("on", data).ConfigureAwait(false); } - catch (System.Exception e) + catch (Exception e) { Logger.LogWarning(e, "Failed to set state from netdaemon switch"); } @@ -1306,33 +1318,31 @@ async Task SetStateOnDaemonAppSwitch(string state, dynamic? data) if (entityId is null) return; - if (!entityId.StartsWith("switch.netdaemon_")) + if (!entityId.StartsWith("switch.netdaemon_", true, CultureInfo.InvariantCulture)) return; // We only want app switches - await SetDependentState(entityId, state); - await ReloadAllApps(); + await SetDependentState(entityId, state).ConfigureAwait(false); + await ReloadAllApps().ConfigureAwait(false); - await PostExternalEvent(new AppsInformationEvent()); + await PostExternalEvent(new AppsInformationEvent()).ConfigureAwait(false); } } private async Task SetDependentState(string entityId, string state) { - var app = _allAppInstances.Values.Where(n => n.EntityId == entityId).FirstOrDefault(); + var app = InternalAllAppInstances.Values.FirstOrDefault(n => n.EntityId == entityId); if (app is not null) { if (state == "off") { // We need to turn off any dependent apps - var depApps = _allAppInstances.Values.Where(n => n.Dependencies.Contains(app.Id)); - - foreach (var depApp in depApps) + foreach (var depApp in InternalAllAppInstances.Values.Where(n => n.Dependencies.Contains(app.Id))) { await SetDependentState(depApp.EntityId, state).ConfigureAwait(false); } app.IsEnabled = false; - await PersistAppStateAsync((NetDaemonAppBase)app); + await PersistAppStateAsync((NetDaemonAppBase)app).ConfigureAwait(false); Logger.LogDebug("SET APP {app} state = disabled", app.Id); } else if (state == "on") @@ -1341,14 +1351,13 @@ private async Task SetDependentState(string entityId, string state) // Enable all apps that this app is dependent on foreach (var depOnId in app.Dependencies) { - var depOnApp = _allAppInstances.Values.Where(n => n.Id == depOnId).FirstOrDefault(); + var depOnApp = InternalAllAppInstances.Values.FirstOrDefault(n => n.Id == depOnId); if (depOnApp is not null) { await SetDependentState(depOnApp.EntityId, state).ConfigureAwait(false); - } } - await PersistAppStateAsync((NetDaemonAppBase)app); + await PersistAppStateAsync((NetDaemonAppBase)app).ConfigureAwait(false); Logger.LogDebug("SET APP {app} state = enabled", app.Id); } } @@ -1364,20 +1373,19 @@ private async Task SetDependentState(string entityId, string state) await SetStateAsync(entityId, state, attributes.ToArray()).ConfigureAwait(false); else await SetStateAsync(entityId, state).ConfigureAwait(false); - } //TODO: Refactor this private async Task PersistAppStateAsync(NetDaemonAppBase app) { - var obj = await GetDataAsync>(app.GetUniqueIdForStorage()).ConfigureAwait(false) ?? new Dictionary(); obj["__IsDisabled"] = !app.IsEnabled; - await SaveDataAsync>(app.GetUniqueIdForStorage(), obj); + await SaveDataAsync(app.GetUniqueIdForStorage(), obj).ConfigureAwait(false); } + [SuppressMessage("", "CA1031")] private async Task RestoreAppState(INetDaemonAppBase appInstance) { try @@ -1412,7 +1420,7 @@ public void SubscribeToExternalEvents(Func func) private async Task PostExternalEvent(ExternalEventBase ev) { - if (_externalEventCallSubscribers.Count == 0) + if (_externalEventCallSubscribers.IsEmpty) return; var callbackTaskList = new List(_externalEventCallSubscribers.Count); @@ -1422,7 +1430,7 @@ private async Task PostExternalEvent(ExternalEventBase ev) callbackTaskList.Add(Task.Run(() => callback(ev))); } - await callbackTaskList.WhenAll(_cancelToken); + await callbackTaskList.WhenAll(_cancelToken).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/Scheduler.cs b/src/Daemon/NetDaemon.Daemon/Daemon/Scheduler.cs index dec352453..049c2f05e 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/Scheduler.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/Scheduler.cs @@ -2,12 +2,14 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NetDaemon.Common; +using NetDaemon.Common.Exceptions; namespace NetDaemon.Daemon { @@ -28,19 +30,19 @@ public class Scheduler : IScheduler /// /// Used to cancel all running tasks /// - private CancellationTokenSource _cancelSource = new CancellationTokenSource(); - + private CancellationTokenSource _cancelSource = new(); + private bool _isStopped; private readonly ConcurrentDictionary _scheduledTasks - = new ConcurrentDictionary(); + = new(); private readonly IManageTime? _timeManager; private readonly ILogger _logger; - private Task _schedulerTask; + private readonly Task _schedulerTask; public Scheduler(IManageTime? timerManager = null, ILoggerFactory? loggerFactory = null) { _timeManager = timerManager ?? new TimeManager(); - loggerFactory = loggerFactory ?? DefaultLoggerFactory; + loggerFactory ??= DefaultLoggerFactory; _logger = loggerFactory.CreateLogger(); _schedulerTask = Task.Run(SchedulerLoop, _cancelSource.Token); } @@ -53,20 +55,17 @@ public Scheduler(IManageTime? timerManager = null, ILoggerFactory? loggerFactory }); /// - public ISchedulerResult RunEvery(int millisecondsDelay, Func func) => RunEveryAsync(millisecondsDelay, func); - - /// - public ISchedulerResult RunEveryAsync(int millisecondsDelay, Func func) + public ISchedulerResult RunEvery(int millisecondsDelay, Func func) { - return RunEveryAsync(TimeSpan.FromMilliseconds(millisecondsDelay), func); + return RunEvery(TimeSpan.FromMilliseconds(millisecondsDelay), func); } /// - public ISchedulerResult RunEvery(TimeSpan timeSpan, Func func) => RunEveryAsync(timeSpan, func); - - /// - public ISchedulerResult RunEveryAsync(TimeSpan timeSpan, Func func) + public ISchedulerResult RunEvery(TimeSpan timeSpan, Func func) { + _ = func ?? + throw new NetDaemonArgumentNullException(nameof(func)); + var cancelSource = new CancellationTokenSource(); var task = RunEveryInternalAsync(timeSpan, func, cancelSource.Token); @@ -75,6 +74,7 @@ public ISchedulerResult RunEveryAsync(TimeSpan timeSpan, Func func) return new SchedulerResult(task, cancelSource); } + [SuppressMessage("", "CA1031")] private async Task RunEveryInternalAsync(TimeSpan timeSpan, Func func, CancellationToken token) { using CancellationTokenSource linkedCts = @@ -131,22 +131,17 @@ internal TimeSpan CalculateEveryMinuteTimeBetweenNowAndTargetTime(short second) } /// - public ISchedulerResult RunDaily(string time, Func func) => RunDailyAsync(time, func); - - /// - public ISchedulerResult RunDaily(string time, IEnumerable? runOnDays, Func func) => - RunDailyAsync(time, runOnDays, func); - - /// - public ISchedulerResult RunDailyAsync(string time, Func func) => RunDailyAsync(time, null, func); + public ISchedulerResult RunDaily(string time, Func func) => RunDaily(time, null, func); /// - public ISchedulerResult RunDailyAsync(string time, IEnumerable? runOnDays, Func func) + public ISchedulerResult RunDaily(string time, IEnumerable? runOnDays, Func func) { + _ = func ?? + throw new NetDaemonArgumentNullException(nameof(func)); + var cancelSource = new CancellationTokenSource(); - DateTime timeOfDayToTrigger; - if (!DateTime.TryParseExact(time, "HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out timeOfDayToTrigger)) + if (!DateTime.TryParseExact(time, "HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime timeOfDayToTrigger)) { throw new FormatException($"{time} is not a valid time for the current locale"); } @@ -157,6 +152,7 @@ public ISchedulerResult RunDailyAsync(string time, IEnumerable? runOn return new SchedulerResult(task, cancelSource); } + [SuppressMessage("", "CA1031")] private async Task RunDailyInternalAsync(DateTime timeOfDayToTrigger, IEnumerable? runOnDays, Func func, CancellationToken token) { using CancellationTokenSource linkedCts = @@ -183,7 +179,9 @@ private async Task RunDailyInternalAsync(DateTime timeOfDayToTrigger, IEnumerabl } } else + { _logger.LogTrace("RunDaily, Time: {time}, parsed time: {timeOfDayToTrigger}, Not run, due to dayofweek", _timeManager!.Current, timeOfDayToTrigger); + } } else { @@ -202,11 +200,10 @@ private async Task RunDailyInternalAsync(DateTime timeOfDayToTrigger, IEnumerabl } /// - public ISchedulerResult RunEveryMinute(short second, Func func) => RunEveryMinuteAsync(second, func); - - /// - public ISchedulerResult RunEveryMinuteAsync(short second, Func func) + public ISchedulerResult RunEveryMinute(short second, Func func) { + _ = func ?? + throw new NetDaemonArgumentNullException(nameof(func)); var cancelSource = new CancellationTokenSource(); var task = RunEveryMinuteInternalAsync(second, func, cancelSource.Token); @@ -215,6 +212,7 @@ public ISchedulerResult RunEveryMinuteAsync(short second, Func func) return new SchedulerResult(task, cancelSource); } + [SuppressMessage("", "CA1031")] private async Task RunEveryMinuteInternalAsync(short second, Func func, CancellationToken token) { using CancellationTokenSource linkedCts = @@ -222,7 +220,6 @@ private async Task RunEveryMinuteInternalAsync(short second, Func func, Ca while (!linkedCts.IsCancellationRequested) { - var now = _timeManager?.Current; var diff = CalculateEveryMinuteTimeBetweenNowAndTargetTime(second); _logger.LogTrace("RunEveryMinute, Delay {diff}", diff); await _timeManager!.Delay(diff, linkedCts.Token).ConfigureAwait(false); @@ -238,20 +235,15 @@ private async Task RunEveryMinuteInternalAsync(short second, Func func, Ca } /// - public ISchedulerResult RunIn(int millisecondsDelay, Func func) => RunInAsync(millisecondsDelay, func); + [SuppressMessage("", "CA1031")] + public ISchedulerResult RunIn(int millisecondsDelay, Func func) => RunIn(TimeSpan.FromMilliseconds(millisecondsDelay), func); /// - public ISchedulerResult RunInAsync(int millisecondsDelay, Func func) + public ISchedulerResult RunIn(TimeSpan timeSpan, Func func) { - return RunInAsync(TimeSpan.FromMilliseconds(millisecondsDelay), func); - } - - /// - public ISchedulerResult RunIn(TimeSpan timeSpan, Func func) => RunInAsync(timeSpan, func); + _ = func ?? + throw new NetDaemonArgumentNullException(nameof(func)); - /// - public ISchedulerResult RunInAsync(TimeSpan timeSpan, Func func) - { var cancelSource = new CancellationTokenSource(); var task = InternalRunInAsync(timeSpan, func, cancelSource.Token); ScheduleTask(task); @@ -259,6 +251,7 @@ public ISchedulerResult RunInAsync(TimeSpan timeSpan, Func func) return new SchedulerResult(task, cancelSource); } + [SuppressMessage("", "CA1031")] private async Task InternalRunInAsync(TimeSpan timeSpan, Func func, CancellationToken token) { using CancellationTokenSource linkedCts = @@ -281,6 +274,11 @@ private async Task InternalRunInAsync(TimeSpan timeSpan, Func func, Cancel /// public async Task Stop() { + if (_isStopped) + return; + + _isStopped = true; + _cancelSource.Cancel(); // Make sure we are waiting for the scheduler task as well @@ -289,9 +287,13 @@ public async Task Stop() var taskResult = await Task.WhenAny( Task.WhenAll(_scheduledTasks.Values.ToArray()), Task.Delay(1000)).ConfigureAwait(false); - if (_scheduledTasks.Values.Count(n => n.IsCompleted == false) > 0) + _cancelSource.Dispose(); + + if (_scheduledTasks.Values.Any(n => !n.IsCompleted)) + { // Todo: Some kind of logging have to be done here to tell user which task caused timeout - throw new ApplicationException("Failed to cancel all tasks"); + throw new NetDaemonException("Failed to cancel all tasks"); + } } /// @@ -301,6 +303,7 @@ public async Task Stop() public async Task Restart() { await Stop().ConfigureAwait(false); + _isStopped = false; _cancelSource = new CancellationTokenSource(); } @@ -309,7 +312,8 @@ private async Task SchedulerLoop() try { while (!_cancelSource.Token.IsCancellationRequested) - if (_scheduledTasks.Count > 0) + { + if (!_scheduledTasks.IsEmpty) { // Make sure we do cleaning and handle new task every 100 ms ScheduleTask(Task.Delay(DefaultSchedulerTimeout, @@ -325,6 +329,7 @@ private async Task SchedulerLoop() { await Task.Delay(DefaultSchedulerTimeout, _cancelSource.Token).ConfigureAwait(false); } + } } catch (OperationCanceledException) {// Normal, just ignore @@ -336,6 +341,7 @@ private void ScheduleTask(Task addedTask) _scheduledTasks[addedTask.Id] = addedTask; } + [SuppressMessage("", "CA1031")] public async ValueTask DisposeAsync() { try @@ -357,7 +363,6 @@ public class TimeManager : IManageTime /// /// Returns current local time /// - /// public DateTime Current => DateTime.Now; /// @@ -386,16 +391,13 @@ public async Task Delay(TimeSpan timeSpan, CancellationToken token) public class SchedulerResult : ISchedulerResult { - private readonly Task _scheduledTask; - private readonly CancellationTokenSource _cancelSource; - public SchedulerResult(Task scheduledTask, CancellationTokenSource cancelSource) { - _scheduledTask = scheduledTask; - _cancelSource = cancelSource; + Task = scheduledTask; + CancelSource = cancelSource; } - public Task Task => _scheduledTask; - public CancellationTokenSource CancelSource => _cancelSource; + public Task Task { get; } + public CancellationTokenSource CancelSource { get; } } } \ No newline at end of file diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/Storage/DataRepository.cs b/src/Daemon/NetDaemon.Daemon/Daemon/Storage/DataRepository.cs index fe4a1a0b7..8ffb496e6 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/Storage/DataRepository.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/Storage/DataRepository.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; +using NetDaemon.Common.Exceptions; namespace NetDaemon.Daemon.Storage { @@ -22,11 +24,11 @@ public DataRepository(string dataStoragePath) } /// + [SuppressMessage("", "CA1031")] public async ValueTask Get(string id) where T : class { try { - var storageJsonFile = Path.Combine(_dataStoragePath, $"{id}_store.json"); if (!File.Exists(storageJsonFile)) @@ -40,7 +42,7 @@ public DataRepository(string dataStoragePath) { } #pragma warning disable CS8603, CS8653 - return default(T); + return default; #pragma warning restore CS8603, CS8653 } @@ -71,7 +73,7 @@ public class ExpandoDictionaryConverter : JsonConverter>(ref reader) - ?? throw new ApplicationException("Null result deserializing dictionary"); + ?? throw new NetDaemonException("Null result deserializing dictionary"); var returnObject = new Dictionary(); var returnDict = (IDictionary)returnObject; foreach (var x in dict.Keys) @@ -95,7 +97,7 @@ public class ExpandoDictionaryConverter : JsonConverter /// - /// public static EntityState Map(this HassState hassState) { + _ = hassState ?? + throw new NetDaemonArgumentNullException(nameof(hassState)); var entityState = new EntityState { EntityId = hassState.EntityId, @@ -37,9 +39,7 @@ private static void MapAttributes(EntityState entityState, HassState hassState) return; // Cast so we can work with the expando object - var dict = entityState.Attribute as IDictionary; - - if (dict == null) + if (entityState.Attribute is not IDictionary dict) throw new ArgumentNullException(nameof(dict), "Expando object should always be dictionary!"); foreach (var (key, value) in hassState.Attributes) diff --git a/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj b/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj index ee532db4a..8faf00c9a 100644 --- a/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj +++ b/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj @@ -25,10 +25,21 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - + - + + ..\..\..\.linting\roslynator.ruleset + + + ..\..\..\.linting\roslynator.ruleset + true + AllEnabledByDefault + \ No newline at end of file diff --git a/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj b/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj index 24dff151e..b33d4e45f 100644 --- a/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj +++ b/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj @@ -19,7 +19,7 @@ First alpha version, expect things to change! Home Assistant - + @@ -33,11 +33,20 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - + - + + + ..\..\..\.linting\roslynator.ruleset + true + AllEnabledByDefault + \ No newline at end of file diff --git a/src/DaemonRunner/DaemonRunner/Infrastructure/Config/NetDaemonConsoleThemes.cs b/src/DaemonRunner/DaemonRunner/Infrastructure/Config/NetDaemonConsoleThemes.cs index fc8fe7146..9c491ab7a 100644 --- a/src/DaemonRunner/DaemonRunner/Infrastructure/Config/NetDaemonConsoleThemes.cs +++ b/src/DaemonRunner/DaemonRunner/Infrastructure/Config/NetDaemonConsoleThemes.cs @@ -8,22 +8,22 @@ public static class NetDaemonConsoleThemes { private static SystemConsoleTheme SystemTheme { get; } = new(new Dictionary { - [ConsoleThemeStyle.Text] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.Gray}, - [ConsoleThemeStyle.SecondaryText] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.DarkGray}, - [ConsoleThemeStyle.TertiaryText] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.DarkGray}, - [ConsoleThemeStyle.Invalid] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.Yellow}, - [ConsoleThemeStyle.Null] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.Green}, - [ConsoleThemeStyle.Name] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.Green}, - [ConsoleThemeStyle.String] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.Green}, - [ConsoleThemeStyle.Number] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.Green}, - [ConsoleThemeStyle.Boolean] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.Green}, - [ConsoleThemeStyle.Scalar] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.Green}, - [ConsoleThemeStyle.LevelVerbose] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.Gray}, - [ConsoleThemeStyle.LevelDebug] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.DarkYellow}, - [ConsoleThemeStyle.LevelInformation] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.DarkGreen}, - [ConsoleThemeStyle.LevelWarning] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.Yellow}, - [ConsoleThemeStyle.LevelError] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.Red}, - [ConsoleThemeStyle.LevelFatal] = new SystemConsoleThemeStyle {Foreground = ConsoleColor.DarkRed}, + [ConsoleThemeStyle.Text] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.Gray }, + [ConsoleThemeStyle.SecondaryText] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.DarkGray }, + [ConsoleThemeStyle.TertiaryText] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.DarkGray }, + [ConsoleThemeStyle.Invalid] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.Yellow }, + [ConsoleThemeStyle.Null] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.Green }, + [ConsoleThemeStyle.Name] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.Green }, + [ConsoleThemeStyle.String] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.Green }, + [ConsoleThemeStyle.Number] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.Green }, + [ConsoleThemeStyle.Boolean] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.Green }, + [ConsoleThemeStyle.Scalar] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.Green }, + [ConsoleThemeStyle.LevelVerbose] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.Gray }, + [ConsoleThemeStyle.LevelDebug] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.DarkYellow }, + [ConsoleThemeStyle.LevelInformation] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.DarkGreen }, + [ConsoleThemeStyle.LevelWarning] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.Yellow }, + [ConsoleThemeStyle.LevelError] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.Red }, + [ConsoleThemeStyle.LevelFatal] = new SystemConsoleThemeStyle { Foreground = ConsoleColor.DarkRed }, }); private static AnsiConsoleTheme AnsiTheme { get; } = new(new Dictionary @@ -48,7 +48,7 @@ public static class NetDaemonConsoleThemes public static ConsoleTheme GetThemeByType(string type) { - return string.Equals(type, "system", StringComparison.InvariantCultureIgnoreCase) ? SystemTheme : AnsiTheme; + return string.Equals(type, "system", StringComparison.OrdinalIgnoreCase) ? SystemTheme : AnsiTheme; } } } \ No newline at end of file diff --git a/src/DaemonRunner/DaemonRunner/Infrastructure/Config/SerilogConfigurator.cs b/src/DaemonRunner/DaemonRunner/Infrastructure/Config/SerilogConfigurator.cs index 336964621..e2f42b2b6 100644 --- a/src/DaemonRunner/DaemonRunner/Infrastructure/Config/SerilogConfigurator.cs +++ b/src/DaemonRunner/DaemonRunner/Infrastructure/Config/SerilogConfigurator.cs @@ -1,6 +1,8 @@ -using System.IO; +using System.Globalization; +using System.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; +using NetDaemon.Common.Exceptions; using Serilog; using Serilog.Core; using Serilog.Events; @@ -9,10 +11,15 @@ namespace NetDaemon.Infrastructure.Config { public static class SerilogConfigurator { - private static readonly LoggingLevelSwitch LevelSwitch = new LoggingLevelSwitch(); + private static readonly LoggingLevelSwitch LevelSwitch = new(); public static LoggerConfiguration Configure(LoggerConfiguration loggerConfiguration, IHostEnvironment hostingEnvironment) { + _ = loggerConfiguration ?? + throw new NetDaemonArgumentNullException(nameof(loggerConfiguration)); + _ = hostingEnvironment ?? + throw new NetDaemonArgumentNullException(nameof(hostingEnvironment)); + var loggingConfiguration = GetLoggingConfiguration(hostingEnvironment); SetMinimumLogLevel(loggingConfiguration.MinimumLevel); @@ -40,7 +47,9 @@ private static LoggingConfiguration GetLoggingConfiguration(IHostEnvironment hos public static void SetMinimumLogLevel(string level) { - LevelSwitch.MinimumLevel = level.ToLower() switch + _ = level ?? + throw new NetDaemonArgumentNullException(nameof(level)); + LevelSwitch.MinimumLevel = level.ToLower(CultureInfo.InvariantCulture) switch { "info" => LogEventLevel.Information, "debug" => LogEventLevel.Debug, diff --git a/src/DaemonRunner/DaemonRunner/Infrastructure/Extensions/AssemblyExtensions.cs b/src/DaemonRunner/DaemonRunner/Infrastructure/Extensions/AssemblyExtensions.cs index 388b0552b..fe0523080 100644 --- a/src/DaemonRunner/DaemonRunner/Infrastructure/Extensions/AssemblyExtensions.cs +++ b/src/DaemonRunner/DaemonRunner/Infrastructure/Extensions/AssemblyExtensions.cs @@ -10,10 +10,7 @@ internal static class AssemblyExtensions public static IEnumerable GetTypesWhereSubclassOf(this Assembly assembly) { return assembly.GetTypes() - .Where(type => type.IsClass) - .Where(type => !type.IsGenericType) - .Where(type => !type.IsAbstract) - .Where(type => type.IsSubclassOf(typeof(T))); + .Where(type => type.IsClass && !type.IsGenericType && !type.IsAbstract && type.IsSubclassOf(typeof(T))); } } } \ No newline at end of file diff --git a/src/DaemonRunner/DaemonRunner/NetDaemonExtensions.cs b/src/DaemonRunner/DaemonRunner/NetDaemonExtensions.cs index c7c1cd562..06b64eb7c 100644 --- a/src/DaemonRunner/DaemonRunner/NetDaemonExtensions.cs +++ b/src/DaemonRunner/DaemonRunner/NetDaemonExtensions.cs @@ -11,15 +11,21 @@ using NetDaemon.Service.App; using Serilog; using NetDaemon.Infrastructure.Config; +using NetDaemon.Common.Exceptions; +using System.Globalization; +using System.Diagnostics.CodeAnalysis; namespace NetDaemon { public static class NetDaemonExtensions { - const string HassioConfigPath = "/data/options.json"; + private const string HassioConfigPath = "/data/options.json"; public static IHostBuilder UseNetDaemon(this IHostBuilder hostBuilder) { + _ = hostBuilder ?? + throw new NetDaemonArgumentNullException(nameof(hostBuilder)); + if (File.Exists(HassioConfigPath)) ReadHassioConfig(); @@ -34,17 +40,14 @@ public static IHostBuilder UseNetDaemon(this IHostBuilder hostBuilder) }) .ConfigureWebHostDefaults(webbuilder => { - webbuilder.UseKestrel(options => { }); + webbuilder.UseKestrel(_ => { }); webbuilder.UseStartup(); }); } public static IHostBuilder UseDefaultNetDaemonLogging(this IHostBuilder hostBuilder) { - return hostBuilder.UseSerilog((context, loggerConfiguration) => - { - SerilogConfigurator.Configure(loggerConfiguration, context.HostingEnvironment); - }); + return hostBuilder.UseSerilog((context, loggerConfiguration) => SerilogConfigurator.Configure(loggerConfiguration, context.HostingEnvironment)); } public static void CleanupNetDaemon() @@ -61,7 +64,7 @@ private static void RegisterNetDaemonAssembly(IServiceCollection services) } /// - /// Returns true if local loading of assemblies should be preferred. + /// Returns true if local loading of assemblies should be preferred. /// This is typically when running in container. When running in dev /// you want the local loading /// @@ -72,17 +75,15 @@ private static bool UseLocalAssemblyLoading() if (string.IsNullOrEmpty(appSource)) return true; - if (appSource.EndsWith(".csproj") || appSource.EndsWith(".dll")) - return true; - else - return false; + return appSource.EndsWith(".csproj", true, CultureInfo.InvariantCulture) + || appSource.EndsWith(".dll", true, CultureInfo.InvariantCulture); } /// /// Reads the Home Assistant (hassio) configuration file /// - /// - static void ReadHassioConfig() + [SuppressMessage("", "CA1031")] + private static void ReadHassioConfig() { try { @@ -101,9 +102,9 @@ static void ReadHassioConfig() Environment.SetEnvironmentVariable("HASSCLIENT_MSGLOGLEVEL", "Default"); _ = hassAddOnSettings?.AppSource ?? - throw new NullReferenceException("AppSource cannot be null"); + throw new NetDaemonNullReferenceException("AppSource cannot be null"); - if (hassAddOnSettings.AppSource.StartsWith("/") || hassAddOnSettings.AppSource[1] == ':') + if (hassAddOnSettings.AppSource.StartsWith("/", true, CultureInfo.InvariantCulture) || hassAddOnSettings.AppSource[1] == ':') { // Hard codede path Environment.SetEnvironmentVariable("NETDAEMON__APPSOURCE", hassAddOnSettings.AppSource); diff --git a/src/DaemonRunner/DaemonRunner/Service/API/ApiData.cs b/src/DaemonRunner/DaemonRunner/Service/API/ApiData.cs index 660d7db1b..9b8d346a0 100644 --- a/src/DaemonRunner/DaemonRunner/Service/API/ApiData.cs +++ b/src/DaemonRunner/DaemonRunner/Service/API/ApiData.cs @@ -4,7 +4,6 @@ namespace NetDaemon.Service.Api { - public class ApiApplication { public string? Id { get; set; } @@ -24,5 +23,4 @@ public class ApiConfig public NetDaemonSettings? DaemonSettings { get; set; } public HomeAssistantSettings? HomeAssistantSettings { get; set; } } - } \ No newline at end of file diff --git a/src/DaemonRunner/DaemonRunner/Service/API/WsHandler.cs b/src/DaemonRunner/DaemonRunner/Service/API/WsHandler.cs index 80b78b244..2efa208ab 100644 --- a/src/DaemonRunner/DaemonRunner/Service/API/WsHandler.cs +++ b/src/DaemonRunner/DaemonRunner/Service/API/WsHandler.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.WebSockets; @@ -13,14 +14,14 @@ using Microsoft.Extensions.Options; using NetDaemon.Common; using NetDaemon.Common.Configuration; +using NetDaemon.Common.Exceptions; using NetDaemon.Daemon; namespace NetDaemon.Service.Api { - public class ApiWebsocketMiddleware { - private static ConcurrentDictionary _sockets = new(); + private static readonly ConcurrentDictionary _sockets = new(); private readonly RequestDelegate _next; @@ -30,7 +31,7 @@ public class ApiWebsocketMiddleware private readonly NetDaemonHost? _host; - private JsonSerializerOptions _jsonOptions = new JsonSerializerOptions + private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; @@ -42,6 +43,10 @@ public class ApiWebsocketMiddleware NetDaemonHost? host = null ) { + _ = netDaemonSettings ?? + throw new NetDaemonArgumentNullException(nameof(netDaemonSettings)); + _ = homeAssistantSettings ?? + throw new NetDaemonArgumentNullException(nameof(homeAssistantSettings)); _logger = loggerFactory.CreateLogger(); _host = host; _netdaemonSettings = netDaemonSettings.Value; @@ -67,34 +72,32 @@ private async Task NewEvent(ExternalEventBase ev) LastErrorMessage = n.IsEnabled ? n.RuntimeInfo.LastErrorMessage : null }) }; - await BroadCast(JsonSerializer.Serialize(eventMessage, _jsonOptions)); + await BroadCast(JsonSerializer.Serialize(eventMessage, _jsonOptions)).ConfigureAwait(false); } } + + [SuppressMessage("", "CA1031")] public async Task Invoke(HttpContext context) { + _ = context ?? + throw new NetDaemonArgumentNullException(nameof(context)); if (!context.WebSockets.IsWebSocketRequest && context.Request.Path != "/api/ws") { - await _next.Invoke(context); + await _next.Invoke(context).ConfigureAwait(false); return; } CancellationToken ct = context.RequestAborted; - WebSocket? currentSocket = await context.WebSockets.AcceptWebSocketAsync(); + WebSocket? currentSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); var socketId = Guid.NewGuid().ToString(); _sockets.TryAdd(socketId, currentSocket); _logger.LogDebug("New websocket client {socketId}", socketId); try { - - while (true) + while (!ct.IsCancellationRequested) { - if (ct.IsCancellationRequested) - { - break; - } - - var msg = await GetNextMessageAsync(currentSocket, ct); + var msg = await GetNextMessageAsync(currentSocket, ct).ConfigureAwait(false); if (currentSocket.State != WebSocketState.Open) { @@ -121,7 +124,7 @@ public async Task Invoke(HttpContext context) }) }; - await BroadCast(JsonSerializer.Serialize(eventMessage, _jsonOptions)); + await BroadCast(JsonSerializer.Serialize(eventMessage, _jsonOptions)).ConfigureAwait(false); break; case "settings": @@ -142,7 +145,7 @@ public async Task Invoke(HttpContext context) Data = tempResult }; - await BroadCast(JsonSerializer.Serialize(settingsMessage, _jsonOptions)); + await BroadCast(JsonSerializer.Serialize(settingsMessage, _jsonOptions)).ConfigureAwait(false); break; case "app": @@ -162,7 +165,7 @@ public async Task Invoke(HttpContext context) } if (command.IsEnabled is not null) { - if (command.IsEnabled ?? false) + if (command.IsEnabled.Value) { _host?.CallService("switch", "turn_on", new { entity_id = $"switch.netdaemon_{msg.App.ToSafeHomeAssistantEntityId()}" }); } @@ -189,12 +192,12 @@ public async Task Invoke(HttpContext context) currentSocket.State == WebSocketState.CloseReceived || currentSocket.State == WebSocketState.CloseSent) { - await currentSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", ct); + await currentSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", ct).ConfigureAwait(false); } currentSocket.Dispose(); } - public async Task BroadCast(string message, CancellationToken ct = default(CancellationToken)) + public async Task BroadCast(string message, CancellationToken ct = default) { _logger.LogTrace("Broadcasting to {count} clients", _sockets.Count); foreach (var socket in _sockets) @@ -204,52 +207,46 @@ public async Task BroadCast(string message, CancellationToken ct = default(Cance continue; } - await SendStringAsync(socket.Value, message, ct); + await SendStringAsync(socket.Value, message, ct).ConfigureAwait(false); } } - private static Task SendStringAsync(WebSocket socket, string data, CancellationToken ct = default(CancellationToken)) + private static Task SendStringAsync(WebSocket socket, string data, CancellationToken ct = default) { var buffer = Encoding.UTF8.GetBytes(data); var segment = new ArraySegment(buffer); return socket.SendAsync(segment, WebSocketMessageType.Text, true, ct); } - private static async Task GetNextMessageAsync(WebSocket socket, CancellationToken ct = default(CancellationToken)) + private static async Task GetNextMessageAsync(WebSocket socket, CancellationToken ct = default) { // System.Text.Json.JsonSerializer.DeserializeAsync(socket.) var buffer = new ArraySegment(new byte[8192]); - _ = buffer.Array ?? throw new NullReferenceException("Failed to allocate memory buffer"); + _ = buffer.Array ?? throw new NetDaemonNullReferenceException("Failed to allocate memory buffer"); - using (var ms = new MemoryStream()) + using var ms = new MemoryStream(); + WebSocketReceiveResult result; + do { - WebSocketReceiveResult result; - do - { - ct.ThrowIfCancellationRequested(); + ct.ThrowIfCancellationRequested(); - result = await socket.ReceiveAsync(buffer, ct); - if (!result.CloseStatus.HasValue) - ms.Write(buffer.Array, buffer.Offset, result.Count); - else - return null; - - } - while (!result.EndOfMessage); - - ms.Seek(0, SeekOrigin.Begin); - if (result.MessageType != WebSocketMessageType.Text) - { + result = await socket.ReceiveAsync(buffer, ct).ConfigureAwait(false); + if (!result.CloseStatus.HasValue) + ms.Write(buffer.Array, buffer.Offset, result.Count); + else return null; - } + } + while (!result.EndOfMessage); - using (var reader = new StreamReader(ms, Encoding.UTF8)) - { - var msgString = await reader.ReadToEndAsync(); - return JsonSerializer.Deserialize(msgString); - } + ms.Seek(0, SeekOrigin.Begin); + if (result.MessageType != WebSocketMessageType.Text) + { + return null; } - } + using var reader = new StreamReader(ms, Encoding.UTF8); + var msgString = await reader.ReadToEndAsync().ConfigureAwait(false); + return JsonSerializer.Deserialize(msgString); + } } } \ No newline at end of file diff --git a/src/DaemonRunner/DaemonRunner/Service/ApiService.cs b/src/DaemonRunner/DaemonRunner/Service/ApiService.cs index 2b92c3295..0aa37a06d 100644 --- a/src/DaemonRunner/DaemonRunner/Service/ApiService.cs +++ b/src/DaemonRunner/DaemonRunner/Service/ApiService.cs @@ -1,20 +1,15 @@ -using System.Reflection; using JoySoftware.HomeAssistant.Client; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using NetDaemon.Daemon; using NetDaemon.Daemon.Storage; -using NetDaemon.Service; using Microsoft.Extensions.Options; using System.IO; using Microsoft.Extensions.Hosting; using NetDaemon.Common; using System; -using System.Net.WebSockets; -using Microsoft.AspNetCore.WebSockets; using NetDaemon.Service.Api; using NetDaemon.Common.Configuration; @@ -22,36 +17,34 @@ namespace NetDaemon.Service { public class ApiStartup { - bool _useAdmin = false; + private readonly bool _useAdmin; public ApiStartup(IConfiguration configuration) { Configuration = configuration; var enableAdminValue = Configuration.GetSection("NetDaemon").GetSection("Admin").Value; - bool.TryParse(enableAdminValue, out _useAdmin); + _ = bool.TryParse(enableAdminValue, out _useAdmin); } public IConfiguration Configuration { get; } - public void ConfigureServices(IServiceCollection services) + public static void ConfigureServices(IServiceCollection services) { - // services.Configure(Context.Configuration.GetSection("HomeAssistant")); - // services.Configure(context.Configuration.GetSection("NetDaemon")); services.AddHostedService(); services.AddTransient(); services.AddTransient(n => new DataRepository( Path.Combine( n.GetRequiredService>().Value.GetAppSourceDirectory() , ".storage"))); - services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddHttpClient(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - if (_useAdmin == true) + if (_useAdmin) { if (env.IsDevelopment()) { diff --git a/src/DaemonRunner/DaemonRunner/Service/App/CodeGenerator.cs b/src/DaemonRunner/DaemonRunner/Service/App/CodeGenerator.cs index b39eebb3e..149e641ee 100644 --- a/src/DaemonRunner/DaemonRunner/Service/App/CodeGenerator.cs +++ b/src/DaemonRunner/DaemonRunner/Service/App/CodeGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using JoySoftware.HomeAssistant.Client; @@ -7,6 +8,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using NetDaemon.Common; +using NetDaemon.Common.Exceptions; using NetDaemon.Common.Reactive; using NetDaemon.Daemon.Config; @@ -14,13 +16,13 @@ namespace NetDaemon.Service.App { - public class CodeGenerator + public static class CodeGenerator { /// - /// Mapps the domain to corresponding implemented Fluent API, will be added as + /// Maps the domain to corresponding implemented Fluent API, will be added as /// more and more entity types are supported /// - private static IDictionary _FluentApiMapper = new Dictionary + private static readonly IDictionary _FluentApiMapper = new Dictionary { ["light"] = ("Entity", "IEntity"), ["script"] = ("Entity", "IEntity"), @@ -29,18 +31,15 @@ public class CodeGenerator ["camera"] = ("Camera", "ICamera"), ["media_player"] = ("MediaPlayer", "IMediaPlayer"), ["automation"] = ("Entity", "IEntity"), - // ["input_boolean"], - // ["remote"], - // ["climate"], }; - public string? GenerateCode(string nameSpace, IEnumerable entities) + public static string? GenerateCode(string nameSpace, IEnumerable entities) { var code = SyntaxFactory.CompilationUnit(); // Add Usings statements code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(typeof(NetDaemonApp).Namespace!))); - code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(typeof(NetDaemon.Common.Fluent.IMediaPlayer).Namespace!))); + code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(typeof(Common.Fluent.IMediaPlayer).Namespace!))); // Add namespace var namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(nameSpace)).NormalizeWhitespace(); @@ -52,16 +51,14 @@ public class CodeGenerator SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword)); // Get all available domains, this is used to create the extensionmethods - var domains = GetDomainsFromEntities(entities); - - foreach (var domain in domains) + foreach (var domain in GetDomainsFromEntities(entities)) { if (_FluentApiMapper.ContainsKey(domain)) { var camelCaseDomain = domain.ToCamelCase(); - var method = $@"public static {camelCaseDomain}Entities {camelCaseDomain}Ex(this NetDaemonApp app) => new {camelCaseDomain}Entities(app);"; + var method = $"public static {camelCaseDomain}Entities {camelCaseDomain}Ex(this NetDaemonApp app) => new {camelCaseDomain}Entities(app);"; var methodDeclaration = CSharpSyntaxTree.ParseText(method).GetRoot().ChildNodes().OfType().FirstOrDefault() - ?? throw new NullReferenceException("Method parsing failed"); + ?? throw new NetDaemonNullReferenceException("Method parsing failed"); extensionClass = extensionClass.AddMembers(methodDeclaration); } @@ -71,7 +68,6 @@ public class CodeGenerator // Add the classes implementing the specific entities foreach (var domain in GetDomainsFromEntities(entities)) { - if (_FluentApiMapper.ContainsKey(domain)) { var classDeclaration = $@"public partial class {domain.ToCamelCase()}Entities @@ -84,11 +80,11 @@ public class CodeGenerator }} }}"; var entityClass = CSharpSyntaxTree.ParseText(classDeclaration).GetRoot().ChildNodes().OfType().FirstOrDefault() - ?? throw new NullReferenceException($"Parse class {nameof(NetDaemonApp)} failed"); - foreach (var entity in entities.Where(n => n.StartsWith(domain))) + ?? throw new NetDaemonNullReferenceException($"Parse class {nameof(NetDaemonApp)} failed"); + foreach (var entity in entities.Where(n => n.StartsWith(domain, true, CultureInfo.InvariantCulture))) { var (fluent, fluentInterface) = _FluentApiMapper[domain]; - var name = entity[(entity.IndexOf(".") + 1)..]; + var name = entity[(entity.IndexOf(".", StringComparison.InvariantCultureIgnoreCase) + 1)..]; // Quick check to make sure the name is a valid C# identifier. Should really check to make // sure it doesn't collide with a reserved keyword as well. if (!char.IsLetter(name[0]) && (name[0] != '_')) @@ -98,10 +94,8 @@ public class CodeGenerator var propertyCode = $@"public {fluentInterface} {name.ToCamelCase()} => _app.{fluent}(""{entity}"");"; var propDeclaration = CSharpSyntaxTree.ParseText(propertyCode).GetRoot().ChildNodes().OfType().FirstOrDefault() - ?? throw new NullReferenceException("Property parsing failed!"); + ?? throw new NetDaemonNullReferenceException("Property parsing failed!"); entityClass = entityClass.AddMembers(propDeclaration); - - } namespaceDeclaration = namespaceDeclaration.AddMembers(entityClass); } @@ -112,7 +106,7 @@ public class CodeGenerator return code.NormalizeWhitespace(indentation: " ", eol: "\n").ToFullString(); } - public string? GenerateCodeRx(string nameSpace, IEnumerable entities, IEnumerable services) + public static string? GenerateCodeRx(string nameSpace, IEnumerable entities, IEnumerable services) { var code = SyntaxFactory.CompilationUnit(); @@ -123,7 +117,7 @@ public class CodeGenerator code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Linq"))); code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(typeof(NetDaemonApp).Namespace!))); code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(typeof(NetDaemonRxApp).Namespace!))); - code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(typeof(NetDaemon.Common.Fluent.FluentExpandoObject).Namespace!))); + code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(typeof(Common.Fluent.FluentExpandoObject).Namespace!))); // Add namespace var namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(nameSpace)).NormalizeWhitespace(); @@ -143,21 +137,20 @@ public class CodeGenerator { var camelCaseDomain = domain.ToCamelCase(); - var isSingleServiceDomain = Array.IndexOf(singleServiceDomains, domain) == 0 ? false : true; + var isSingleServiceDomain = Array.IndexOf(singleServiceDomains, domain) != 0; var property = isSingleServiceDomain ? - $@"public {camelCaseDomain}Entities {camelCaseDomain} => new {camelCaseDomain}Entities(this);" : + $"public {camelCaseDomain}Entities {camelCaseDomain} => new {camelCaseDomain}Entities(this);" : $@"public {camelCaseDomain}Entity {camelCaseDomain} => new {domain.ToCamelCase()}Entity(this, new string[] {{""""}});"; var propertyDeclaration = CSharpSyntaxTree.ParseText(property).GetRoot().ChildNodes().OfType().FirstOrDefault() - ?? throw new NullReferenceException($"Parse of property {camelCaseDomain} Entities/Entity failed"); + ?? throw new NetDaemonNullReferenceException($"Parse of property {camelCaseDomain} Entities/Entity failed"); extensionClass = extensionClass.AddMembers(propertyDeclaration); } namespaceDeclaration = namespaceDeclaration.AddMembers(extensionClass); foreach (var domain in GetDomainsFromEntities(entities)) { - var classDeclaration = $@"public partial class {domain.ToCamelCase()}Entity : RxEntity {{ public string EntityId => EntityIds.First(); @@ -165,7 +158,7 @@ public class CodeGenerator public string? Area => DaemonRxApp?.State(EntityId)?.Area; public dynamic? Attribute => DaemonRxApp?.State(EntityId)?.Attribute; - + public DateTime? LastChanged => DaemonRxApp?.State(EntityId)?.LastChanged; public DateTime? LastUpdated => DaemonRxApp?.State(EntityId)?.LastUpdated; @@ -173,11 +166,11 @@ public class CodeGenerator public dynamic? State => DaemonRxApp?.State(EntityId)?.State; public {domain.ToCamelCase()}Entity(INetDaemonReactive daemon, IEnumerable entityIds) : base(daemon, entityIds) - {{ + {{ }} }}"; var entityClass = CSharpSyntaxTree.ParseText(classDeclaration).GetRoot().ChildNodes().OfType().FirstOrDefault() - ?? throw new NullReferenceException("Failed to parse class declaration"); + ?? throw new NetDaemonNullReferenceException("Failed to parse class declaration"); // They allready have default implementation var skipServices = new string[] { "turn_on", "turn_off", "toggle" }; @@ -186,7 +179,7 @@ public class CodeGenerator if (s.Service is null) continue; - var name = s.Service[(s.Service.IndexOf(".") + 1)..]; + var name = s.Service[(s.Service.IndexOf(".", StringComparison.InvariantCultureIgnoreCase) + 1)..]; if (Array.IndexOf(skipServices, name) >= 0) continue; @@ -197,7 +190,7 @@ public class CodeGenerator { name = "s_" + name; } - var hasEntityId = (s.Fields is not null && s.Fields.Count(c => c.Field == "entity_id") > 0) ? true : false; + var hasEntityId = s.Fields is not null && s.Fields.Any(c => c.Field == "entity_id"); var entityAssignmentStatement = hasEntityId ? @"serviceData[""entity_id""] = EntityId;" : ""; var methodCode = $@"public void {name.ToCamelCase()}(dynamic? data=null) @@ -207,7 +200,7 @@ public class CodeGenerator if (data is ExpandoObject) {{ serviceData.CopyFrom(data); - }} + }} else if (data is not null) {{ var expObject = ((object)data).ToExpandoObject(); @@ -219,18 +212,15 @@ public class CodeGenerator }} "; var methodDeclaration = CSharpSyntaxTree.ParseText(methodCode).GetRoot().ChildNodes().OfType().FirstOrDefault() - ?? throw new NullReferenceException("Failed to parse method"); + ?? throw new NetDaemonNullReferenceException("Failed to parse method"); entityClass = entityClass.AddMembers(methodDeclaration); - } namespaceDeclaration = namespaceDeclaration.AddMembers(entityClass); - } // Add the classes implementing the specific entities foreach (var domain in GetDomainsFromEntities(entities)) { - var classDeclaration = $@"public partial class {domain.ToCamelCase()}Entities {{ private readonly {nameof(NetDaemonRxApp)} _app; @@ -241,11 +231,10 @@ public class CodeGenerator }} }}"; var entityClass = CSharpSyntaxTree.ParseText(classDeclaration).GetRoot().ChildNodes().OfType().FirstOrDefault() - ?? throw new NullReferenceException("Failed to parse entity class"); - foreach (var entity in entities.Where(n => n.StartsWith(domain))) + ?? throw new NetDaemonNullReferenceException("Failed to parse entity class"); + foreach (var entity in entities.Where(n => n.StartsWith(domain, StringComparison.InvariantCultureIgnoreCase))) { - - var name = entity[(entity.IndexOf(".") + 1)..]; + var name = entity[(entity.IndexOf(".", StringComparison.InvariantCultureIgnoreCase) + 1)..]; // Quick check to make sure the name is a valid C# identifier. Should really check to make // sure it doesn't collide with a reserved keyword as well. if (!char.IsLetter(name[0]) && (name[0] != '_')) @@ -255,7 +244,7 @@ public class CodeGenerator var propertyCode = $@"public {domain.ToCamelCase()}Entity {name.ToCamelCase()} => new {domain.ToCamelCase()}Entity(_app, new string[] {{""{entity}""}});"; var propDeclaration = CSharpSyntaxTree.ParseText(propertyCode).GetRoot().ChildNodes().OfType().FirstOrDefault() - ?? throw new NullReferenceException("Failed to parse property"); + ?? throw new NetDaemonNullReferenceException("Failed to parse property"); entityClass = entityClass.AddMembers(propDeclaration); } namespaceDeclaration = namespaceDeclaration.AddMembers(entityClass); @@ -271,6 +260,6 @@ public class CodeGenerator /// /// A list of entities internal static IEnumerable GetDomainsFromEntities(IEnumerable entities) => - entities.Select(n => n[0..n.IndexOf(".")]).Distinct(); + entities.Select(n => n[0..n.IndexOf(".", StringComparison.InvariantCultureIgnoreCase)]).Distinct(); } } \ No newline at end of file diff --git a/src/DaemonRunner/DaemonRunner/Service/App/DaemonAppCompiler.cs b/src/DaemonRunner/DaemonRunner/Service/App/DaemonAppCompiler.cs index 445700e4f..175a27731 100644 --- a/src/DaemonRunner/DaemonRunner/Service/App/DaemonAppCompiler.cs +++ b/src/DaemonRunner/DaemonRunner/Service/App/DaemonAppCompiler.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NetDaemon.Common; using NetDaemon.Common.Configuration; +using NetDaemon.Common.Exceptions; using NetDaemon.Infrastructure.Extensions; namespace NetDaemon.Service.App @@ -14,17 +14,19 @@ namespace NetDaemon.Service.App public class DaemonAppCompiler : IDaemonAppCompiler { private readonly ILogger _logger; - private readonly IOptions _netDaemonSettings; - private string? _sourceFolder = null; + private readonly string? _sourceFolder; public DaemonAppCompiler(ILogger logger, IOptions netDaemonSettings) { + _ = netDaemonSettings ?? + throw new NetDaemonArgumentNullException(nameof(netDaemonSettings)); _logger = logger; - _netDaemonSettings = netDaemonSettings; + NetDaemonSettings = netDaemonSettings; _sourceFolder = netDaemonSettings.Value.GetAppSourceDirectory(); - } + public IOptions NetDaemonSettings { get; } + public IEnumerable GetApps() { _logger.LogDebug("Loading dynamically compiled apps..."); @@ -41,8 +43,7 @@ public IEnumerable GetApps() public Assembly Load() { - CollectibleAssemblyLoadContext alc; - return DaemonCompiler.GetCompiledAppAssembly(out alc, _sourceFolder!, _logger); + return DaemonCompiler.GetCompiledAppAssembly(out _, _sourceFolder!, _logger); } } } \ No newline at end of file diff --git a/src/DaemonRunner/DaemonRunner/Service/App/DaemonCompiler.cs b/src/DaemonRunner/DaemonRunner/Service/App/DaemonCompiler.cs index 815104cbd..df7180e85 100644 --- a/src/DaemonRunner/DaemonRunner/Service/App/DaemonCompiler.cs +++ b/src/DaemonRunner/DaemonRunner/Service/App/DaemonCompiler.cs @@ -27,7 +27,7 @@ public CollectibleAssemblyLoadContext() : base(isCollectible: true) { } - protected override Assembly? Load(AssemblyName _) => null; + protected override Assembly? Load(AssemblyName assemblyName) => null; } /// @@ -39,13 +39,12 @@ public static (IEnumerable, CollectibleAssemblyLoadContext?) GetDaemonApps { var loadedApps = new List(50); - CollectibleAssemblyLoadContext alc; // Load the compiled apps - var (compiledApps, compileErrorText) = GetCompiledApps(out alc, codeFolder, logger); + var (compiledApps, compileErrorText) = GetCompiledApps(out CollectibleAssemblyLoadContext alc, codeFolder, logger); if (compiledApps is not null) loadedApps.AddRange(compiledApps); - else if (string.IsNullOrEmpty(compileErrorText) == false) + else if (!string.IsNullOrEmpty(compileErrorText)) logger.LogError(compileErrorText); else if (loadedApps.Count == 0) logger.LogWarning("No .cs files files found, please add files to netdaemonfolder {codeFolder}", codeFolder); @@ -79,22 +78,20 @@ public static Assembly GetCompiledAppAssembly(out CollectibleAssemblyLoadContext InterceptAppInfo(syntaxTree, compilation); } - using (var peStream = new MemoryStream()) - { - var emitResult = compilation.Emit(peStream: peStream); + using var peStream = new MemoryStream(); + var emitResult = compilation.Emit(peStream: peStream); - if (emitResult.Success) - { - peStream.Seek(0, SeekOrigin.Begin); + if (emitResult.Success) + { + peStream.Seek(0, SeekOrigin.Begin); - return alc!.LoadFromStream(peStream); - } - else - { - PrettyPrintCompileError(emitResult, logger); + return alc!.LoadFromStream(peStream); + } + else + { + PrettyPrintCompileError(emitResult, logger); - return null!; - } + return null!; } } finally @@ -111,20 +108,13 @@ private static List LoadSyntaxTree(string codeFolder) var result = new List(50); // Get the paths for all .cs files recursively in app folder - var csFiles = Directory.EnumerateFiles(codeFolder, "*.cs", SearchOption.AllDirectories); - - foreach (var csFile in csFiles) - { - using (var fs = new FileStream(csFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - { - var sourceText = SourceText.From(fs, encoding: Encoding.UTF8, canBeEmbedded: true); - var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, path: csFile); - result.Add(syntaxTree); - } - } - + IEnumerable? csFiles = Directory.EnumerateFiles(codeFolder, "*.cs", SearchOption.AllDirectories); + result.AddRange(from csFile in csFiles + let fs = new FileStream(csFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) + let sourceText = SourceText.From(fs, encoding: Encoding.UTF8, canBeEmbedded: true) + let syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, path: csFile) + select syntaxTree); return result; - } public static IEnumerable GetDefaultReferences() @@ -135,7 +125,7 @@ public static IEnumerable GetDefaultReferences() MetadataReference.CreateFromFile(typeof(System.Text.RegularExpressions.Regex).Assembly.Location), MetadataReference.CreateFromFile(typeof(Console).Assembly.Location), MetadataReference.CreateFromFile(typeof(System.ComponentModel.DataAnnotations.DisplayAttribute).Assembly.Location), - MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location), MetadataReference.CreateFromFile(typeof(System.ComponentModel.INotifyPropertyChanged).Assembly.Location), MetadataReference.CreateFromFile(typeof(System.Linq.Expressions.DynamicExpression).Assembly.Location), MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.Logging.Abstractions.NullLogger).Assembly.Location), @@ -149,14 +139,13 @@ public static IEnumerable GetDefaultReferences() MetadataReference.CreateFromFile(typeof(System.Reactive.Linq.Observable).Assembly.Location), }; - var assembliesFromCurrentAppDomain = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var assembly in assembliesFromCurrentAppDomain) + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { if (!assembly.IsDynamic && !string.IsNullOrEmpty(assembly.Location)) metaDataReference.Add(MetadataReference.CreateFromFile(assembly.Location)); } - metaDataReference.Add(MetadataReference.CreateFromFile(Assembly.GetEntryAssembly()?.Location!)); + metaDataReference.Add(MetadataReference.CreateFromFile((Assembly.GetEntryAssembly()?.Location)!)); return metaDataReference; } @@ -166,7 +155,6 @@ private static CSharpCompilation GetCsCompilation(string codeFolder) var syntaxTrees = LoadSyntaxTree(codeFolder); var metaDataReference = GetDefaultReferences(); - return CSharpCompilation.Create( $"net_{Path.GetRandomFileName()}.dll", syntaxTrees.ToArray(), @@ -182,7 +170,7 @@ private static CSharpCompilation GetCsCompilation(string codeFolder) private static void PrettyPrintCompileError(EmitResult emitResult, ILogger logger) { var msg = new StringBuilder(); - msg.AppendLine($"Compiler error!"); + msg.AppendLine("Compiler error!"); foreach (var emitResultDiagnostic in emitResult.Diagnostics) { @@ -216,9 +204,7 @@ private static void InterceptAppInfo(SyntaxTree syntaxTree, CSharpCompilation co { var semModel = compilation.GetSemanticModel(syntaxTree); - var classDeclarationExpressions = syntaxTree.GetRoot().DescendantNodes().OfType(); - - foreach (var classDeclaration in classDeclarationExpressions) + foreach (var classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType()) { var symbol = semModel?.GetDeclaredSymbol(classDeclaration); @@ -249,7 +235,7 @@ private static void InterceptAppInfo(SyntaxTree syntaxTree, CSharpCompilation co comment += commentRow + "\n"; } - var app_key = symbol.ContainingNamespace.Name == "" ? symbol.Name : symbol.ContainingNamespace.Name + "." + symbol.Name; + var app_key = symbol.ContainingNamespace.Name?.Length == 0 ? symbol.Name : symbol.ContainingNamespace.Name + "." + symbol.Name; if (!NetDaemonAppBase.CompileTimeProperties.ContainsKey(app_key)) { @@ -281,16 +267,18 @@ private static void WarnIfExecuteIsMissing(SyntaxTree syntaxTree, CSharpCompilat if (symbol is null) continue; - if (string.IsNullOrEmpty(symbol?.Name) || - ExecuteWarningOnInvocationNames.Contains(symbol?.Name) == false) + if (string.IsNullOrEmpty(symbol.Name) || + !ExecuteWarningOnInvocationNames.Contains(symbol.Name)) + { // The invocation name is empty or not in list of invocations // that needs to be closed with Execute or ExecuteAsync continue; + } // Now find top invocation to match whole expression InvocationExpressionSyntax topInvocationExpression = invocationExpression; - if (symbol is not null && symbol.ContainingType.Name == "NetDaemonApp") + if (symbol.ContainingType.Name == "NetDaemonApp") { var disableLogging = false; @@ -300,28 +288,25 @@ private static void WarnIfExecuteIsMissing(SyntaxTree syntaxTree, CSharpCompilat while (parentInvocationExpression is not null) { - if (parentInvocationExpression is MethodDeclarationSyntax) + if (parentInvocationExpression is MethodDeclarationSyntax methodDeclarationSyntax && ExpressionContainsDisableLogging(methodDeclarationSyntax)) { - if (ExpressionContainsDisableLogging((MethodDeclarationSyntax)parentInvocationExpression)) - { - disableLogging = true; - } + disableLogging = true; } - if (parentInvocationExpression is InvocationExpressionSyntax) + if (parentInvocationExpression is InvocationExpressionSyntax invocationExpressionSyntax) { var parentSymbol = (IMethodSymbol?)semModel?.GetSymbolInfo(invocationExpression).Symbol; if (parentSymbol?.Name == symbolName) - topInvocationExpression = (InvocationExpressionSyntax)parentInvocationExpression; + topInvocationExpression = invocationExpressionSyntax; } parentInvocationExpression = parentInvocationExpression.Parent; } // Now when we have the top InvocationExpression, // lets check for Execute and ExecuteAsync - if (ExpressionContainsExecuteInvocations(topInvocationExpression) == false && disableLogging == false) + if (!ExpressionContainsExecuteInvocations(topInvocationExpression) && !disableLogging) { var x = syntaxTree.GetLineSpan(topInvocationExpression.Span); - if (linesReported.Contains(x.StartLinePosition.Line) == false) + if (!linesReported.Contains(x.StartLinePosition.Line)) { logger.LogError($"Missing Execute or ExecuteAsync in {syntaxTree.FilePath} ({x.StartLinePosition.Line + 1},{x.StartLinePosition.Character + 1}) near {topInvocationExpression.ToFullString().Trim()}"); linesReported.Add(x.StartLinePosition.Line); @@ -335,11 +320,8 @@ private static void WarnIfExecuteIsMissing(SyntaxTree syntaxTree, CSharpCompilat private static bool ExpressionContainsDisableLogging(MethodDeclarationSyntax methodInvocationExpression) { var invocationString = methodInvocationExpression.ToFullString(); - if (invocationString.Contains("[DisableLog") && invocationString.Contains("SupressLogType.MissingExecute")) - { - return true; - } - return false; + return invocationString.Contains("[DisableLog", StringComparison.InvariantCultureIgnoreCase) + && invocationString.Contains("SupressLogType.MissingExecute", StringComparison.InvariantCultureIgnoreCase); } // Todo: Refactor using something smarter than string match. In the future use Roslyn @@ -347,12 +329,8 @@ private static bool ExpressionContainsExecuteInvocations(InvocationExpressionSyn { var invocationString = invocation.ToFullString(); - if (invocationString.Contains("ExecuteAsync()") || invocationString.Contains("Execute()")) - { - return true; - } - - return false; + return invocationString.Contains("ExecuteAsync()", StringComparison.InvariantCultureIgnoreCase) + || invocationString.Contains("Execute()", StringComparison.InvariantCultureIgnoreCase); } } } \ No newline at end of file diff --git a/src/DaemonRunner/DaemonRunner/Service/App/IDaemonAppCompiler.cs b/src/DaemonRunner/DaemonRunner/Service/App/IDaemonAppCompiler.cs index f4a6498ea..7f737d7ae 100644 --- a/src/DaemonRunner/DaemonRunner/Service/App/IDaemonAppCompiler.cs +++ b/src/DaemonRunner/DaemonRunner/Service/App/IDaemonAppCompiler.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Reflection; namespace NetDaemon.Service.App { @@ -10,7 +9,6 @@ public interface IDaemonAppCompiler /// Temporary /// /// - [Obsolete("Only exists while migrating the world to IOC.")] IEnumerable GetApps(); } } \ No newline at end of file diff --git a/src/DaemonRunner/DaemonRunner/Service/App/LocalDaemonAppCompiler.cs b/src/DaemonRunner/DaemonRunner/Service/App/LocalDaemonAppCompiler.cs index ddd6027ad..f78bbf6b6 100644 --- a/src/DaemonRunner/DaemonRunner/Service/App/LocalDaemonAppCompiler.cs +++ b/src/DaemonRunner/DaemonRunner/Service/App/LocalDaemonAppCompiler.cs @@ -26,10 +26,10 @@ public IEnumerable GetApps() var assemblies = LoadAll(); var apps = assemblies.SelectMany(x => x.GetTypesWhereSubclassOf()).ToList(); - if (!apps.Any()) + if (apps.Count == 0) _logger.LogWarning("No local daemon apps found."); else - _logger.LogDebug("Found total of {nr_of_apps} apps", apps.Count()); + _logger.LogDebug("Found total of {nr_of_apps} apps", apps.Count); return apps; } diff --git a/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs b/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs index b63e8c587..6b17fb3fc 100644 --- a/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs +++ b/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading; @@ -9,6 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NetDaemon.Common.Configuration; +using NetDaemon.Common.Exceptions; using NetDaemon.Daemon; using NetDaemon.Daemon.Config; using NetDaemon.Service.App; @@ -35,9 +37,9 @@ public class RunnerService : BackgroundService private bool _entitiesGenerated; private IEnumerable? _loadedDaemonApps; - private string? _sourcePath = null; + private string? _sourcePath; - private bool _hasConnectedBefore = false; + private bool _hasConnectedBefore; public RunnerService( ILoggerFactory loggerFactory, @@ -48,6 +50,10 @@ public class RunnerService : BackgroundService IDaemonAppCompiler daemonAppCompiler ) { + _ = homeAssistantSettings ?? + throw new NetDaemonArgumentNullException(nameof(homeAssistantSettings)); + _ = netDaemonSettings ?? + throw new NetDaemonArgumentNullException(nameof(netDaemonSettings)); _logger = loggerFactory.CreateLogger(); _homeAssistantSettings = homeAssistantSettings.Value; _netDaemonSettings = netDaemonSettings.Value; @@ -62,6 +68,7 @@ public override async Task StopAsync(CancellationToken cancellationToken) await base.StopAsync(cancellationToken).ConfigureAwait(false); } + [SuppressMessage("", "CA1031")] protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try @@ -88,9 +95,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _loadedDaemonApps = null; await using var daemonHost = _serviceProvider.GetService() - ?? throw new ApplicationException("Failed to get service for NetDaemonHost"); + ?? throw new NetDaemonException("Failed to get service for NetDaemonHost"); { - await Run(daemonHost, stoppingToken); + await Run(daemonHost, stoppingToken).ConfigureAwait(false); } } catch (OperationCanceledException) @@ -104,6 +111,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogInformation("NetDaemon service exited!"); } + [SuppressMessage("", "CA1031")] private async Task Run(NetDaemonHost daemonHost, CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) @@ -126,7 +134,7 @@ private async Task Run(NetDaemonHost daemonHost, CancellationToken stoppingToken stoppingToken ); - if (await WaitForDaemonToConnect(daemonHost, stoppingToken).ConfigureAwait(false) == false) + if (!await WaitForDaemonToConnect(daemonHost, stoppingToken).ConfigureAwait(false)) { continue; } @@ -139,12 +147,12 @@ private async Task Run(NetDaemonHost daemonHost, CancellationToken stoppingToken { // Generate code if requested if (_sourcePath is string) - await GenerateEntities(daemonHost, _sourcePath); + await GenerateEntities(daemonHost, _sourcePath).ConfigureAwait(false); if (_loadedDaemonApps is null) _loadedDaemonApps = _daemonAppCompiler.GetApps(); - if (_loadedDaemonApps is null || !_loadedDaemonApps.Any()) + if (_loadedDaemonApps?.Any() != true) { _logger.LogError("No NetDaemon apps could be found, exiting..."); return; @@ -155,7 +163,6 @@ private async Task Run(NetDaemonHost daemonHost, CancellationToken stoppingToken // Wait until daemon stops await daemonHostTask.ConfigureAwait(false); - } catch (TaskCanceledException) { @@ -199,7 +206,6 @@ private async Task Run(NetDaemonHost daemonHost, CancellationToken stoppingToken // If we reached here it could be a re-connect _hasConnectedBefore = true; - } } private async Task GenerateEntities(NetDaemonHost daemonHost, string sourceFolder) @@ -213,16 +219,15 @@ private async Task GenerateEntities(NetDaemonHost daemonHost, string sourceFolde _logger.LogTrace("Generating entities from Home Assistant instance .."); _entitiesGenerated = true; - var codeGen = new CodeGenerator(); - var source = codeGen.GenerateCode( + var source = CodeGenerator.GenerateCode( "Netdaemon.Generated.Extensions", daemonHost.State.Select(n => n.EntityId).Distinct() ); await File.WriteAllTextAsync(Path.Combine(sourceFolder!, "_EntityExtensions.cs.gen"), source).ConfigureAwait(false); - var services = await daemonHost.GetAllServices(); - var sourceRx = codeGen.GenerateCodeRx( + var services = await daemonHost.GetAllServices().ConfigureAwait(false); + var sourceRx = CodeGenerator.GenerateCodeRx( "Netdaemon.Generated.Reactive", daemonHost.State.Select(n => n.EntityId).Distinct(), services @@ -231,7 +236,7 @@ private async Task GenerateEntities(NetDaemonHost daemonHost, string sourceFolde await File.WriteAllTextAsync(Path.Combine(sourceFolder!, "_EntityExtensionsRx.cs.gen"), sourceRx).ConfigureAwait(false); } - private async Task WaitForDaemonToConnect(NetDaemonHost daemonHost, CancellationToken stoppingToken) + private static async Task WaitForDaemonToConnect(NetDaemonHost daemonHost, CancellationToken stoppingToken) { var nrOfTimesCheckForConnectedState = 0; diff --git a/src/DevelopmentApps/NetDaemon.DevelopmentApps.csproj b/src/DevelopmentApps/NetDaemon.DevelopmentApps.csproj index 0394c7cfe..cfd71b893 100644 --- a/src/DevelopmentApps/NetDaemon.DevelopmentApps.csproj +++ b/src/DevelopmentApps/NetDaemon.DevelopmentApps.csproj @@ -19,4 +19,7 @@ + + ..\..\.linting\roslynator.ruleset + \ No newline at end of file diff --git a/src/DevelopmentApps/apps/DebugApp/DebugApp.cs b/src/DevelopmentApps/apps/DebugApp/DebugApp.cs index c0bcccb35..7305fd1e0 100644 --- a/src/DevelopmentApps/apps/DebugApp/DebugApp.cs +++ b/src/DevelopmentApps/apps/DebugApp/DebugApp.cs @@ -4,14 +4,13 @@ namespace NetDaemon.DevelopmentApps.apps.DebugApp { - - /// Use this class as startingpoint for debugging + /// Use this class as startingpoint for debugging /// public class DebugApp : NetDaemonRxApp { // Use two guids, one when instanced and one when initialized // can track errors with instancing - Guid _instanceId = Guid.NewGuid(); + private Guid _instanceId = Guid.NewGuid(); public DebugApp() : base() { } @@ -28,5 +27,4 @@ public void CallMeFromHass(dynamic data) Log("A call from hass! {data}", data); } } - } diff --git a/src/Fakes/NetDaemon.Fakes/DaemonHostTestBase.cs b/src/Fakes/NetDaemon.Fakes/DaemonHostTestBase.cs index b82cc3ba0..052bde16c 100644 --- a/src/Fakes/NetDaemon.Fakes/DaemonHostTestBase.cs +++ b/src/Fakes/NetDaemon.Fakes/DaemonHostTestBase.cs @@ -7,6 +7,7 @@ using JoySoftware.HomeAssistant.Client; using Moq; using NetDaemon.Common; +using NetDaemon.Common.Exceptions; using NetDaemon.Common.Fluent; using NetDaemon.Common.Reactive; using NetDaemon.Daemon.Storage; @@ -14,64 +15,61 @@ namespace NetDaemon.Daemon.Fakes { - /// /// Base class for test classes /// - public partial class DaemonHostTestBase : IAsyncLifetime + public class DaemonHostTestBase : IAsyncLifetime { - private readonly NetDaemonHost _defaultDaemonHost; - private readonly Mock _defaultDataRepositoryMock; - private readonly HassClientMock _defaultHassClientMock; - private readonly HttpHandlerMock _defaultHttpHandlerMock; - private readonly LoggerMock _loggerMock; private Task? _fakeConnectedDaemon; + private CancellationTokenSource? _cancelSource; /// /// Default contructor /// public DaemonHostTestBase() { - _loggerMock = new LoggerMock(); - _defaultHassClientMock = HassClientMock.DefaultMock; - _defaultDataRepositoryMock = new Mock(); - - _defaultHttpHandlerMock = new HttpHandlerMock(); - _defaultDaemonHost = new NetDaemonHost( - _defaultHassClientMock.Object, - _defaultDataRepositoryMock.Object, - _loggerMock.LoggerFactory, - _defaultHttpHandlerMock.Object); - - _defaultDaemonHost.InternalDelayTimeForTts = 0; // Allow no extra waittime + LoggerMock = new LoggerMock(); + DefaultHassClientMock = HassClientMock.DefaultMock; + DefaultDataRepositoryMock = new Mock(); + + DefaultHttpHandlerMock = new HttpHandlerMock(); + DefaultDaemonHost = new NetDaemonHost( + DefaultHassClientMock.Object, + DefaultDataRepositoryMock.Object, + LoggerMock.LoggerFactory, + DefaultHttpHandlerMock.Object) + { + InternalDelayTimeForTts = 0 // Allow no extra waittime + }; } /// /// Returns default DaemonHost mock /// - public NetDaemonHost DefaultDaemonHost => _defaultDaemonHost; + public NetDaemonHost DefaultDaemonHost { get; } /// /// Returns default data repository mock /// - public Mock DefaultDataRepositoryMock => _defaultDataRepositoryMock; + public Mock DefaultDataRepositoryMock { get; } /// /// Returns default HassClient mock /// - public HassClientMock DefaultHassClientMock => _defaultHassClientMock; + public HassClientMock DefaultHassClientMock { get; } /// /// Returns default HttpHandler mock /// - public HttpHandlerMock DefaultHttpHandlerMock => _defaultHttpHandlerMock; + public HttpHandlerMock DefaultHttpHandlerMock { get; } /// /// Returns default logger mock /// - public LoggerMock LoggerMock => _loggerMock; + public LoggerMock LoggerMock { get; } /// /// Cleans up test /// public Task DisposeAsync() { + _cancelSource?.Dispose(); return Task.CompletedTask; } @@ -79,10 +77,9 @@ public Task DisposeAsync() /// Gets a object as dynamic /// /// The object to turn into dynamic - public dynamic GetDynamicDataObject(string testData = "testdata") + public static dynamic GetDynamicDataObject(string testData = "testdata") { - var expandoObject = new ExpandoObject(); - dynamic dynamicData = expandoObject; + dynamic dynamicData = new ExpandoObject(); dynamicData.Test = testData; return dynamicData; } @@ -90,7 +87,7 @@ public dynamic GetDynamicDataObject(string testData = "testdata") /// /// Converts parameters to dynamics /// - public (dynamic, FluentExpandoObject) GetDynamicObject(params (string, object)[] dynamicParameters) + public static (dynamic, FluentExpandoObject) GetDynamicObject(params (string, object)[] dynamicParameters) { var expandoObject = new FluentExpandoObject(); var dict = expandoObject as IDictionary; @@ -116,6 +113,8 @@ public virtual Task InitializeAsync() /// The state to sett public void SetEntityState(HassState state) { + _ = state ?? + throw new NetDaemonArgumentNullException(nameof(state)); DefaultHassClientMock.FakeStates[state.EntityId] = state; } @@ -127,14 +126,12 @@ public void SetEntityState(HassState state) /// Area of entity public void SetEntityState(string entityId, dynamic? state = null, string? area = null) { - var entity = new EntityState + DefaultDaemonHost.InternalState[entityId] = new EntityState { EntityId = entityId, Area = area, State = state }; - - DefaultDaemonHost.InternalState[entityId] = entity; } /// @@ -143,11 +140,13 @@ public void SetEntityState(string entityId, dynamic? state = null, string? area /// The instance of the app to add public async Task AddAppInstance(INetDaemonAppBase app) { + _ = app ?? + throw new NetDaemonArgumentNullException(nameof(app)); if (string.IsNullOrEmpty(app.Id)) app.Id = Guid.NewGuid().ToString(); DefaultDaemonHost.InternalAllAppInstances[app.Id] = app; DefaultDaemonHost.InternalRunningAppInstances[app.Id] = app; - await app.StartUpAsync(DefaultDaemonHost); + await app.StartUpAsync(DefaultDaemonHost).ConfigureAwait(false); } /// @@ -248,7 +247,7 @@ public void VerifyEventSent(string ev, object? eventData) /// Data sent by service /// If service was waiting for response /// Number of times called - public void VerifyCallService(string domain, string service, object? data = null, bool waitForResponse = false, Moq.Times? times = null) + public void VerifyCallService(string domain, string service, object? data = null, bool waitForResponse = false, Times? times = null) { DefaultHassClientMock.VerifyCallService(domain, service, data, waitForResponse, times); } @@ -259,15 +258,16 @@ public void VerifyCallService(string domain, string service, object? data = null /// Service domain /// Service to verify /// EntityId to verify - /// Data sent by service /// If service was waiting for response /// Number of times called /// Attributes to verify - public void VerifyCallService(string domain, string service, string entityId, object? data = null, bool waitForResponse = false, Moq.Times? times = null, + public void VerifyCallService(string domain, string service, string entityId, bool waitForResponse = false, Times? times = null, params (string attribute, object value)[] attributesTuples) { - var serviceObject = new FluentExpandoObject(); - serviceObject["entity_id"] = entityId; + var serviceObject = new FluentExpandoObject + { + ["entity_id"] = entityId + }; foreach (var (attr, val) in attributesTuples) { serviceObject[attr] = val; @@ -301,8 +301,7 @@ public void VerifyCallServiceTimes(string service, Times times) /// Wait for task until canceled /// /// Task to wait for - /// - private async Task WaitUntilCanceled(Task task) + private static async Task WaitUntilCanceled(Task task) { try { @@ -321,12 +320,12 @@ private async Task InitApps() { foreach (var inst in DefaultDaemonHost.InternalAllAppInstances) { - await inst.Value.StartUpAsync(_defaultDaemonHost); + await inst.Value.StartUpAsync(DefaultDaemonHost).ConfigureAwait(false); } foreach (var inst in DefaultDaemonHost.InternalRunningAppInstances) { - await inst.Value.InitializeAsync(); + await inst.Value.InitializeAsync().ConfigureAwait(false); inst.Value.Initialize(); } } @@ -344,7 +343,7 @@ private async Task InitApps() } /// - /// Verifies that state being set + /// Verifies that state being set /// /// Unique identifier of the entity /// How many times it being set @@ -360,17 +359,16 @@ public void VerifySetStateTimes(string entityId, Times times) /// True if running debug mode should not cancel on timeout protected async Task InitializeFakeDaemon(short timeout = 300, bool overrideDebugNotCancel = false) { - _fakeConnectedDaemon = await GetConnectedNetDaemonTask(timeout, overrideDebugNotCancel); + _fakeConnectedDaemon = await GetConnectedNetDaemonTask(timeout, overrideDebugNotCancel).ConfigureAwait(false); } /// - /// + /// Runs the fake daemon service until timed out /// - /// protected async Task RunFakeDaemonUntilTimeout() { _ = _fakeConnectedDaemon ?? - throw new NullReferenceException("No task to process, did you forget to run InitFakeDaemon at the beginning of the test?"); + throw new NetDaemonNullReferenceException("No task to process, did you forget to run InitFakeDaemon at the beginning of the test?"); await WaitUntilCanceled(_fakeConnectedDaemon).ConfigureAwait(false); } @@ -380,21 +378,20 @@ protected async Task RunFakeDaemonUntilTimeout() /// /// Timeout in milliseconds /// True to use timeout while debugging - /// private async Task GetConnectedNetDaemonTask(short milliSeconds = 100, bool overrideDebugNotCancel = false) { - var cancelSource = Debugger.IsAttached && !overrideDebugNotCancel + _cancelSource = Debugger.IsAttached && !overrideDebugNotCancel ? new CancellationTokenSource() : new CancellationTokenSource(milliSeconds); - await InitApps(); + await InitApps().ConfigureAwait(false); - var daemonTask = _defaultDaemonHost.Run("host", 8123, false, "token", cancelSource.Token); - await WaitForDefaultDaemonToConnect(DefaultDaemonHost, cancelSource.Token); + var daemonTask = DefaultDaemonHost.Run("host", 8123, false, "token", _cancelSource.Token); + await WaitForDefaultDaemonToConnect(DefaultDaemonHost, _cancelSource.Token).ConfigureAwait(false); return daemonTask; } - private async Task WaitForDefaultDaemonToConnect(NetDaemonHost daemonHost, CancellationToken cancellationToken) + private static async Task WaitForDefaultDaemonToConnect(NetDaemonHost daemonHost, CancellationToken cancellationToken) { var nrOfTimesCheckForConnectedState = 0; diff --git a/src/Fakes/NetDaemon.Fakes/HassClientMock.cs b/src/Fakes/NetDaemon.Fakes/HassClientMock.cs index a96fa7575..03b3e9c71 100644 --- a/src/Fakes/NetDaemon.Fakes/HassClientMock.cs +++ b/src/Fakes/NetDaemon.Fakes/HassClientMock.cs @@ -9,6 +9,7 @@ using NetDaemon.Common; using NetDaemon.Common.Fluent; using Xunit; +using NetDaemon.Common.Exceptions; namespace NetDaemon.Daemon.Fakes { @@ -20,28 +21,27 @@ public class HassClientMock : Mock /// /// Fake areas in HassClient /// - /// - public HassAreas Areas = new HassAreas(); + public HassAreas Areas { get; } = new(); /// /// Fake devices in HassClient /// /// - public HassDevices Devices = new HassDevices(); + public HassDevices Devices { get; } = new(); /// /// Fake entities in HassClient /// - public HassEntities Entities = new HassEntities(); + public HassEntities Entities { get; } = new(); /// /// Fake events in HassClient /// - public ConcurrentQueue FakeEvents = new(); + public ConcurrentQueue FakeEvents { get; } = new(); /// /// All current fake entities and it's states /// - public ConcurrentDictionary FakeStates = new(); + public ConcurrentDictionary FakeStates { get; } = new(); /// /// Default constructor @@ -57,13 +57,13 @@ public HassClientMock() SetupGet(x => x.States).Returns(FakeStates); Setup(x => x.GetAllStates(It.IsAny())) - .ReturnsAsync(() => { return (IEnumerable)FakeStates.Values; }); + .ReturnsAsync(() => FakeStates.Values); Setup(x => x.ReadEventAsync()) - .ReturnsAsync(() => { return FakeEvents.TryDequeue(out var ev) ? ev : null; }); + .ReturnsAsync(() => FakeEvents.TryDequeue(out var ev) ? ev : null); Setup(x => x.ReadEventAsync(It.IsAny())) - .ReturnsAsync(() => { return FakeEvents.TryDequeue(out var ev) ? ev : null; }); + .ReturnsAsync(() => FakeEvents.TryDequeue(out var ev) ? ev : null); Setup(x => x.GetConfig()).ReturnsAsync(new HassConfig { State = "RUNNING" }); @@ -93,7 +93,7 @@ public HassClientMock() Areas.Add(new HassArea { Name = "Area", Id = "area_idd" }); Entities.Add(new HassEntity { - EntityId = "light.ligth_in_area", + EntityId = "light.light_in_area", DeviceId = "device_idd" }); } @@ -101,7 +101,7 @@ public HassClientMock() /// /// Default instance of mock /// - public static HassClientMock DefaultMock => new HassClientMock(); + public static HassClientMock DefaultMock => new(); /// /// Returns a mock that will always return false when connect to Home Assistant @@ -126,7 +126,7 @@ public static HassClientMock MockConnectFalse /// Data sent by service public void AddCallServiceEvent(string domain, string service, dynamic? data = null) { - // Todo: Refactor to smth smarter + // Todo: Refactor to something smarter FakeEvents.Enqueue(new HassEvent { EventType = "call_service", @@ -149,7 +149,7 @@ public void AddCallServiceEvent(string domain, string service, dynamic? data = n /// Last changed public void AddChangedEvent(string entityId, object fromState, object toState, DateTime lastUpdated, DateTime lastChanged) { - // Todo: Refactor to smth smarter + // Todo: Refactor to something smarter FakeEvents.Enqueue(new HassEvent { EventType = "state_changed", @@ -250,12 +250,16 @@ public void AddCustomEvent(string eventType, dynamic? data) } /// - /// Assert if the HassClient entity state is equeal to NetDaemon entity state + /// Assert if the HassClient entity state is equals to NetDaemon entity state /// /// HassClient state instance /// NetDaemon state instance - public void AssertEqual(HassState hassState, EntityState entity) + public static void AssertEqual(HassState hassState, EntityState entity) { + _ = hassState ?? + throw new NetDaemonArgumentNullException(nameof(hassState)); + _ = entity ?? + throw new NetDaemonArgumentNullException(nameof(entity)); Assert.Equal(hassState.EntityId, entity.EntityId); Assert.Equal(hassState.State, entity.State); Assert.Equal(hassState.LastChanged, entity.LastChanged); @@ -267,7 +271,7 @@ public void AssertEqual(HassState hassState, EntityState entity) foreach (var attribute in hassState.Attributes!.Keys) { var attr = entity.Attribute as IDictionary ?? - throw new NullReferenceException($"{nameof(entity.Attribute)} catn be null"); + throw new NetDaemonNullReferenceException($"{nameof(entity.Attribute)} cant be null"); Assert.True(attr.ContainsKey(attribute)); Assert.Equal(hassState.Attributes[attribute], @@ -279,10 +283,9 @@ public void AssertEqual(HassState hassState, EntityState entity) /// Gets a cancellation source that does not timeout if debugger is attached /// /// - /// - public CancellationTokenSource GetSourceWithTimeout(int milliSeconds = 100) + public static CancellationTokenSource GetSourceWithTimeout(int milliSeconds = 100) { - return (Debugger.IsAttached) + return Debugger.IsAttached ? new CancellationTokenSource() : new CancellationTokenSource(milliSeconds); } @@ -297,8 +300,8 @@ public CancellationTokenSource GetSourceWithTimeout(int milliSeconds = 100) params (string attribute, object value)[] attributesTuples) { var attributes = new FluentExpandoObject(); - foreach (var attributesTuple in attributesTuples) - ((IDictionary)attributes)[attributesTuple.attribute] = attributesTuple.value; + foreach (var (attribute, value) in attributesTuples) + attributes[attribute] = value; Verify(n => n.CallService(domain, service, attributes, It.IsAny()), Times.AtLeastOnce); } @@ -311,13 +314,12 @@ public CancellationTokenSource GetSourceWithTimeout(int milliSeconds = 100) /// Data sent by service /// If service was waiting for response /// Number of times called - public void VerifyCallService(string domain, string service, object? data = null, bool waitForResponse = false, Moq.Times? times = null) + public void VerifyCallService(string domain, string service, object? data = null, bool waitForResponse = false, Times? times = null) { if (times is not null) Verify(n => n.CallService(domain, service, data!, waitForResponse), times.Value); else Verify(n => n.CallService(domain, service, data!, waitForResponse), Times.AtLeastOnce); - } /// @@ -327,7 +329,7 @@ public void VerifyCallService(string domain, string service, object? data = null /// Service to verify /// If service was waiting for response /// Number of times called - public void VerifyCallService(string domain, string service, bool waitForResponse = false, Moq.Times? times = null) + public void VerifyCallService(string domain, string service, bool waitForResponse = false, Times? times = null) { if (times is not null) Verify(n => n.CallService(domain, service, It.IsAny(), waitForResponse), times.Value); @@ -345,8 +347,6 @@ public void VerifyCallServiceTimes(string service, Times times) Verify(n => n.CallService(It.IsAny(), service, It.IsAny(), It.IsAny()), times); } - - /// /// Verify state if entity /// @@ -357,8 +357,8 @@ public void VerifyCallServiceTimes(string service, Times times) params (string attribute, object value)[] attributesTuples) { var attributes = new FluentExpandoObject(); - foreach (var attributesTuple in attributesTuples) - ((IDictionary)attributes)[attributesTuple.attribute] = attributesTuple.value; + foreach (var (attribute, value) in attributesTuples) + attributes[attribute] = value; Verify(n => n.SetState(entity, state, attributes), Times.AtLeastOnce); } diff --git a/src/Fakes/NetDaemon.Fakes/HttpHandlerMock.cs b/src/Fakes/NetDaemon.Fakes/HttpHandlerMock.cs index 5c3595dc7..6a884070a 100644 --- a/src/Fakes/NetDaemon.Fakes/HttpHandlerMock.cs +++ b/src/Fakes/NetDaemon.Fakes/HttpHandlerMock.cs @@ -5,13 +5,14 @@ using System.Threading; using System.Threading.Tasks; using NetDaemon.Common; +using System; namespace NetDaemon.Daemon.Fakes { /// /// HttpClient Mock /// - public class HttpClientFactoryMock : Mock + public class HttpClientFactoryMock : Mock, IDisposable { private HttpClient? _httpClient; private MockHttpMessageHandler? _handler; @@ -38,12 +39,34 @@ public void SetResponse(string response, HttpStatusCode statusCode = HttpStatusC _httpClient = new HttpClient(_handler); Setup(x => x.CreateClient(It.IsAny())).Returns(_httpClient!); } + + /// + /// Dispose + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the object and cancel delay + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Make sure any subscriptions are canceled + _httpClient?.Dispose(); + _handler?.Dispose(); + } + } } /// /// Mock of HttpHandler /// - public class HttpHandlerMock : Mock + public class HttpHandlerMock : Mock, IDisposable { private HttpClient? _httpClient; private MockHttpMessageHandler? _handler; @@ -71,6 +94,28 @@ public void SetResponse(string response, HttpStatusCode statusCode = HttpStatusC _httpClient = new HttpClient(_handler); Setup(x => x.CreateHttpClient(It.IsAny())).Returns(_httpClient!); } + + /// + /// Dispose + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the object and cancel delay + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Make sure any subscriptions are canceled + _handler?.Dispose(); + _httpClient?.Dispose(); + } + } } /// @@ -110,7 +155,7 @@ protected override async Task SendAsync(HttpRequestMessage var responseMessage = new HttpResponseMessage(_StatusCode); if (request is not null && request.Content is not null) - _requestContent = await request.Content.ReadAsStringAsync().ConfigureAwait(false); + _requestContent = await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); responseMessage.Content = new ByteArrayContent(Encoding.ASCII.GetBytes(_response)); return responseMessage; diff --git a/src/Fakes/NetDaemon.Fakes/LoggerMock.cs b/src/Fakes/NetDaemon.Fakes/LoggerMock.cs index eb58b1ee3..d6c071ece 100644 --- a/src/Fakes/NetDaemon.Fakes/LoggerMock.cs +++ b/src/Fakes/NetDaemon.Fakes/LoggerMock.cs @@ -47,9 +47,9 @@ public void AssertLogged(LogLevel level, Times times) x => x.Log( level, It.IsAny(), - It.Is((v, t) => true), + It.Is((_, __) => true), It.IsAny(), - It.Is>((v, t) => true)), times); + It.Is>((_, _) => true)), times); } /// @@ -64,9 +64,9 @@ public void AssertLogged(LogLevel level, string message, Times times) x => x.Log( level, It.IsAny(), - It.Is((v, t) => v.ToString() == message), + It.Is((v, _) => v.ToString() == message), It.IsAny(), - It.Is>((v, t) => true)), times); + It.Is>((_, _) => true)), times); } /// @@ -82,9 +82,9 @@ public void AssertLogged(LogLevel level, Exception exception, string message, Ti x => x.Log( level, It.IsAny(), - It.Is((v, t) => v.ToString() == message), + It.Is((v, _) => v.ToString() == message), exception, - It.Is>((v, t) => true)), times); + It.Is>((_, _) => true)), times); } } } \ No newline at end of file diff --git a/src/Fakes/NetDaemon.Fakes/NetDaemon.Fakes.csproj b/src/Fakes/NetDaemon.Fakes/NetDaemon.Fakes.csproj index 037b002e4..48ae107c0 100644 --- a/src/Fakes/NetDaemon.Fakes/NetDaemon.Fakes.csproj +++ b/src/Fakes/NetDaemon.Fakes/NetDaemon.Fakes.csproj @@ -34,5 +34,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + ..\..\..\.linting\roslynator.ruleset + true + AllEnabledByDefault + diff --git a/src/Service/Program.cs b/src/Service/Program.cs index 8bd342b42..855a3f100 100644 --- a/src/Service/Program.cs +++ b/src/Service/Program.cs @@ -8,11 +8,13 @@ await Host.CreateDefaultBuilder(args) .UseDefaultNetDaemonLogging() .UseNetDaemon() .Build() - .RunAsync(); + .RunAsync() + .ConfigureAwait(false); } catch (Exception e) { Console.WriteLine($"Failed to start host... {e}"); + throw; } finally { diff --git a/src/Service/Service.csproj b/src/Service/Service.csproj index d4d95834c..6fd8a3804 100644 --- a/src/Service/Service.csproj +++ b/src/Service/Service.csproj @@ -9,6 +9,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + @@ -24,4 +28,9 @@ Always + + ..\..\.linting\roslynator.ruleset + true + AllEnabledByDefault + diff --git a/tests/NetDaemon.Daemon.Tests/Daemon/DataRepositoryTests.cs b/tests/NetDaemon.Daemon.Tests/Daemon/DataRepositoryTests.cs index 3f17c1bfa..08aa24e0c 100644 --- a/tests/NetDaemon.Daemon.Tests/Daemon/DataRepositoryTests.cs +++ b/tests/NetDaemon.Daemon.Tests/Daemon/DataRepositoryTests.cs @@ -13,10 +13,10 @@ namespace NetDaemon.Daemon.Tests.Daemon { public class DataRepositoryTests : DaemonHostTestBase { - public static readonly string DataReposityryPath = + public static readonly string DataRepositoryPath = Path.Combine(AppContext.BaseDirectory, "datarepository"); - public DataRepositoryTests() : base() + public DataRepositoryTests() { } @@ -27,7 +27,7 @@ public async Task GetNonExistantValueShouldReturnNull() var daemon = DefaultDaemonHost; // ACT - var data = await daemon.GetDataAsync("not_exists"); + var data = await daemon.GetDataAsync("not_exists").ConfigureAwait(false); // ASSERT Assert.Null(data); } @@ -41,8 +41,8 @@ public async Task SavedDataShouldReturnSameDataUsingExpando() data.Item = "Some data"; // ACT - await daemon.SaveDataAsync("data_exists", data); - var collectedData = await daemon.GetDataAsync("data_exists"); + await daemon.SaveDataAsync("data_exists", data).ConfigureAwait(false); + var collectedData = await daemon.GetDataAsync("data_exists").ConfigureAwait(false); // ASSERT Assert.Equal(data, collectedData); @@ -55,9 +55,9 @@ public async Task GetDataShouldReturnCachedValue() var daemon = DefaultDaemonHost; // ACT - await daemon.SaveDataAsync("GetDataShouldReturnCachedValue_id", "saved data"); + await daemon.SaveDataAsync("GetDataShouldReturnCachedValue_id", "saved data").ConfigureAwait(false); - await daemon.GetDataAsync("GetDataShouldReturnCachedValue_id"); + await daemon.GetDataAsync("GetDataShouldReturnCachedValue_id").ConfigureAwait(false); // ASSERT DefaultDataRepositoryMock.Verify(n => n.Get(It.IsAny()), Times.Never); @@ -68,35 +68,27 @@ public async Task GetDataShouldReturnCachedValue() public async Task RepositoryLoadSavedDataUsingExpando() { // ARRANGE - var dataRepository = new DataRepository(DataReposityryPath); + var dataRepository = new DataRepository(DataRepositoryPath); dynamic dataBeingSaved = new FluentExpandoObject(false, true); - var now = DateTime.Now; dataBeingSaved.SomeString = "this data should be saved!"; dataBeingSaved.SomeInt = 123456; dataBeingSaved.SomeFloat = 1.23456; dataBeingSaved.SomeDateTime = DateTime.Now; // ACT - await dataRepository.Save>("RepositoryLoadSavedData_id", dataBeingSaved); + await dataRepository.Save>("RepositoryLoadSavedData_id", dataBeingSaved).ConfigureAwait(false); - var dataReturned = await dataRepository.Get>("RepositoryLoadSavedData_id"); + var dataReturned = await dataRepository.Get>("RepositoryLoadSavedData_id").ConfigureAwait(false); var returnedFluentExpandoObject = new FluentExpandoObject(); returnedFluentExpandoObject.CopyFrom(dataReturned!); dynamic dynamicDataReturned = returnedFluentExpandoObject; - var x = dataBeingSaved.SomeString; - var y = dynamicDataReturned!.SomeString; - - if (dataBeingSaved.SomeString == dynamicDataReturned?.SomeString) - { - System.Console.WriteLine("hello"); - } // ASSERT - Assert.Equal(dataBeingSaved.SomeString, dynamicDataReturned?.SomeString); + Assert.Equal(dataBeingSaved.SomeString, dynamicDataReturned.SomeString); Assert.Equal(dataBeingSaved.SomeInt, dynamicDataReturned!.SomeInt); Assert.Equal(dataBeingSaved.SomeFloat, dynamicDataReturned!.SomeFloat); - // There is no way for json serilizer to know this is a datetime + // There is no way for json serializer to know this is a datetime Assert.NotNull(dynamicDataReturned!.SomeDateTime); } @@ -104,7 +96,7 @@ public async Task RepositoryLoadSavedDataUsingExpando() public async Task RepositoryShouldLoadSavedDataUsingDto() { // ARRANGE - var dataRepository = new DataRepository(DataReposityryPath); + var dataRepository = new DataRepository(DataRepositoryPath); var storeData = new TestStorage { AString = "Some String", @@ -112,9 +104,9 @@ public async Task RepositoryShouldLoadSavedDataUsingDto() ADateTime = DateTime.Now }; // ACT - await dataRepository.Save("RepositoryShouldLoadSavedDataUsingDto_id", storeData); + await dataRepository.Save("RepositoryShouldLoadSavedDataUsingDto_id", storeData).ConfigureAwait(false); - var dataReturned = await dataRepository.Get("RepositoryShouldLoadSavedDataUsingDto_id"); + var dataReturned = await dataRepository.Get("RepositoryShouldLoadSavedDataUsingDto_id").ConfigureAwait(false); // ASSERT Assert.Equal(storeData.AString, dataReturned!.AString); diff --git a/tests/NetDaemon.Daemon.Tests/Daemon/ExtensionMethodsTests.cs b/tests/NetDaemon.Daemon.Tests/Daemon/ExtensionMethodsTests.cs index ec883bb21..97991bb31 100644 --- a/tests/NetDaemon.Daemon.Tests/Daemon/ExtensionMethodsTests.cs +++ b/tests/NetDaemon.Daemon.Tests/Daemon/ExtensionMethodsTests.cs @@ -19,7 +19,7 @@ public void JsonElementToDynamicValueWhenStringShouldReturnString() var prop = doc.RootElement.GetProperty("str"); // ACT - var obj = JsonElementExtensions.ConvertToDynamicValue(prop); + var obj = prop.ConvertToDynamicValue(); // ASSERT Assert.IsType(obj); @@ -34,7 +34,7 @@ public void JsonElementToDynamicValueWhenTrueShouldReturnBoolTrue() var prop = doc.RootElement.GetProperty("bool"); // ACT - var obj = JsonElementExtensions.ConvertToDynamicValue(prop); + var obj = prop.ConvertToDynamicValue(); // ASSERT Assert.IsType(obj); @@ -49,7 +49,7 @@ public void JsonElementToDynamicValueWhenFalseShouldReturnBoolFalse() var prop = doc.RootElement.GetProperty("bool"); // ACT - var obj = JsonElementExtensions.ConvertToDynamicValue(prop); + var obj = prop.ConvertToDynamicValue(); // ASSERT Assert.IsType(obj); @@ -62,9 +62,9 @@ public void JsonElementToDynamicValueWhenIntegerShouldReturnInteger() // ARRANGE var doc = JsonDocument.Parse("{\"int\": 10}"); var prop = doc.RootElement.GetProperty("int"); - long expectedValue = 10; + const long expectedValue = 10; // ACT - var obj = JsonElementExtensions.ConvertToDynamicValue(prop); + var obj = prop.ConvertToDynamicValue(); // ASSERT Assert.IsType(obj); @@ -77,9 +77,9 @@ public void JsonElementToDynamicValueWhenDoubleShouldReturnDouble() // ARRANGE var doc = JsonDocument.Parse("{\"int\": 10.5}"); var prop = doc.RootElement.GetProperty("int"); - double expectedValue = 10.5; + const double expectedValue = 10.5; // ACT - var obj = JsonElementExtensions.ConvertToDynamicValue(prop); + var obj = prop.ConvertToDynamicValue(); // ASSERT Assert.IsType(obj); @@ -94,7 +94,7 @@ public void JsonElementToDynamicValueWhenArrayShouldReturnArray() var prop = doc.RootElement.GetProperty("array"); // ACT - var obj = JsonElementExtensions.ConvertToDynamicValue(prop); + var obj = prop.ConvertToDynamicValue(); var arr = obj as IEnumerable; // ASSERT @@ -109,7 +109,7 @@ public void JsonElementToDynamicValueWhenObjectShouldReturnObject() var doc = JsonDocument.Parse("{\"str\": \"string\"}"); // ACT - var obj = JsonElementExtensions.ConvertToDynamicValue(doc.RootElement) as IDictionary; + var obj = doc.RootElement.ConvertToDynamicValue() as IDictionary; // ASSERT Assert.Equal("string", obj?["str"]); @@ -119,7 +119,7 @@ public void JsonElementToDynamicValueWhenObjectShouldReturnObject() public void ParseDataTypeIntShouldReturnInt() { // ARRANGE - long expectedValue = 10; + const long expectedValue = 10; // ACT var longValue = StringParser.ParseDataType("10"); // ASSERT @@ -131,12 +131,12 @@ public void ParseDataTypeIntShouldReturnInt() public void ParseDataTypeDoubleShouldReturnInt() { // ARRANGE - double expectedValue = 10.5; + const double expectedValue = 10.5; // ACT - var doubeValue = StringParser.ParseDataType("10.5"); + var doubleValue = StringParser.ParseDataType("10.5"); // ASSERT - Assert.IsType(doubeValue); - Assert.Equal(expectedValue, doubeValue); + Assert.IsType(doubleValue); + Assert.Equal(expectedValue, doubleValue); } [Fact] diff --git a/tests/NetDaemon.Daemon.Tests/Daemon/HttpHandlerMock.cs b/tests/NetDaemon.Daemon.Tests/Daemon/HttpHandlerMock.cs deleted file mode 100644 index a44db111c..000000000 --- a/tests/NetDaemon.Daemon.Tests/Daemon/HttpHandlerMock.cs +++ /dev/null @@ -1,74 +0,0 @@ -// using Moq; -// using System.Net; -// using System.Net.Http; -// using System.Text; -// using System.Threading; -// using System.Threading.Tasks; -// using NetDaemon.Common; - -// namespace NetDaemon.Daemon.Tests.Daemon -// { -// public class HttpClientFactoryMock : Mock -// { -// private HttpClient? _httpClient; -// private MockHttpMessageHandler? _handler; -// public MockHttpMessageHandler? MessageHandler => _handler; - -// public HttpClientFactoryMock() -// { -// } - -// public void SetResponse(string response, HttpStatusCode statusCode = HttpStatusCode.OK) -// { -// _handler = new MockHttpMessageHandler(response, statusCode); -// _httpClient = new HttpClient(_handler); -// Setup(x => x.CreateClient(It.IsAny())).Returns(_httpClient!); -// } -// } - -// public class HttpHandlerMock : Mock -// { -// private HttpClient? _httpClient; -// private MockHttpMessageHandler? _handler; - -// public MockHttpMessageHandler? MessageHandler => _handler; - -// public HttpHandlerMock() -// { -// } - -// public void SetResponse(string response, HttpStatusCode statusCode = HttpStatusCode.OK) -// { -// _handler = new MockHttpMessageHandler(response, statusCode); -// _httpClient = new HttpClient(_handler); -// Setup(x => x.CreateHttpClient(It.IsAny())).Returns(_httpClient!); -// } -// } - -// public class MockHttpMessageHandler : HttpMessageHandler -// { -// private readonly string _response; -// private readonly HttpStatusCode _StatusCode; - -// private string? _requestContent; - -// public string? RequestContent => _requestContent; - -// public MockHttpMessageHandler(string response, HttpStatusCode statusCode = HttpStatusCode.OK) -// { -// _response = response; -// _StatusCode = statusCode; -// } - -// protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) -// { -// var responseMessage = new HttpResponseMessage(_StatusCode); - -// if (request is not null && request.Content is not null) -// _requestContent = await request.Content.ReadAsStringAsync().ConfigureAwait(false); - -// responseMessage.Content = new ByteArrayContent(Encoding.ASCII.GetBytes(_response)); -// return responseMessage; -// } -// } -// } \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/Daemon/HttpTests.cs b/tests/NetDaemon.Daemon.Tests/Daemon/HttpTests.cs index 7f3be7b0e..dabcce3b5 100644 --- a/tests/NetDaemon.Daemon.Tests/Daemon/HttpTests.cs +++ b/tests/NetDaemon.Daemon.Tests/Daemon/HttpTests.cs @@ -15,15 +15,11 @@ public class SerializedReturn public class HttpTests : DaemonHostTestBase { - public HttpTests() : base() - { - } - [Fact] public async Task HttpClientShouldReturnCorrectContent() { // ARRANGE - var response = "{\"json_prop\", \"hello world\"}"; + const string? response = "{\"json_prop\", \"hello world\"}"; DefaultHttpHandlerMock.SetResponse(response); // ACT @@ -38,7 +34,7 @@ public async Task HttpClientShouldReturnCorrectContent() public void HttpClientShouldNotReturnContentOnBadStatusCode() { // ARRANGE - var response = ""; + const string? response = ""; DefaultHttpHandlerMock.SetResponse(response, HttpStatusCode.NotFound); // ACT @@ -52,7 +48,7 @@ public void HttpClientShouldNotReturnContentOnBadStatusCode() public async Task HttpClientShouldReturnCorrectStatusCode() { // ARRANGE - var response = "{\"json_prop\", \"hello world\"}"; + const string? response = "{\"json_prop\", \"hello world\"}"; DefaultHttpHandlerMock.SetResponse(response); // ACT @@ -67,7 +63,7 @@ public async Task HttpClientShouldReturnCorrectStatusCode() public async Task HttpClientShouldReturnCorrectStatusCodeError() { // ARRANGE - var response = "{\"json_prop\", \"hello world\"}"; + const string? response = "{\"json_prop\", \"hello world\"}"; DefaultHttpHandlerMock.SetResponse(response, HttpStatusCode.Forbidden); // ACT @@ -82,9 +78,9 @@ public async Task HttpClientShouldReturnCorrectStatusCodeError() public async Task HttpHandlerGetJsonShouldReturnCorrectContent() { // ARRANGE - var response = "{\"json_prop\": \"hello world\"}"; + const string? response = "{\"json_prop\": \"hello world\"}"; - HttpClientFactoryMock factoryMock = new HttpClientFactoryMock(); + using HttpClientFactoryMock factoryMock = new(); factoryMock.SetResponse(response); var httpHandler = new HttpHandler(factoryMock.Object); @@ -100,9 +96,9 @@ public async Task HttpHandlerGetJsonShouldReturnCorrectContent() public void HttpHandlerGetJsonBadFormatShouldReturnThrowException() { // ARRANGE - var response = "{\"json_prop\": \"hello world\"}"; + const string? response = "{\"json_prop\": \"hello world\"}"; - HttpClientFactoryMock factoryMock = new HttpClientFactoryMock(); + using HttpClientFactoryMock factoryMock = new(); factoryMock.SetResponse(response); var httpHandler = new HttpHandler(factoryMock.Object); @@ -115,9 +111,9 @@ public void HttpHandlerGetJsonBadFormatShouldReturnThrowException() public async Task HttpHandlerPostJsonShouldReturnCorrectContent() { // ARRANGE - var response = "{\"json_prop\": \"hello world\"}"; + const string? response = "{\"json_prop\": \"hello world\"}"; - HttpClientFactoryMock factoryMock = new HttpClientFactoryMock(); + using HttpClientFactoryMock factoryMock = new(); factoryMock.SetResponse(response); var httpHandler = new HttpHandler(factoryMock.Object); @@ -127,16 +123,16 @@ public async Task HttpHandlerPostJsonShouldReturnCorrectContent() // ASSERT Assert.Equal("hello world", result?.Property); - Assert.Equal("{\"posted\":\"some value\"}", factoryMock?.MessageHandler?.RequestContent); + Assert.Equal("{\"posted\":\"some value\"}", factoryMock.MessageHandler?.RequestContent); } [Fact] public async Task HttpHandlerPostJsonNoReturnShouldReturnCorrectContent() { // ARRANGE - var response = "{\"json_prop\": \"hello world\"}"; + const string? response = "{\"json_prop\": \"hello world\"}"; - HttpClientFactoryMock factoryMock = new HttpClientFactoryMock(); + using HttpClientFactoryMock factoryMock = new(); factoryMock.SetResponse(response); var httpHandler = new HttpHandler(factoryMock.Object); @@ -145,7 +141,7 @@ public async Task HttpHandlerPostJsonNoReturnShouldReturnCorrectContent() await httpHandler.PostJson("http://fake.com", new { posted = "some value" }).ConfigureAwait(false); // ASSERT - Assert.Equal("{\"posted\":\"some value\"}", factoryMock?.MessageHandler?.RequestContent); + Assert.Equal("{\"posted\":\"some value\"}", factoryMock.MessageHandler?.RequestContent); } } } \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/Daemon/NetDaemonHostTests.cs b/tests/NetDaemon.Daemon.Tests/Daemon/NetDaemonHostTests.cs index a6ac578f4..dd3eb91f8 100644 --- a/tests/NetDaemon.Daemon.Tests/Daemon/NetDaemonHostTests.cs +++ b/tests/NetDaemon.Daemon.Tests/Daemon/NetDaemonHostTests.cs @@ -8,6 +8,8 @@ using NetDaemon.Common; using Xunit; using NetDaemon.Daemon.Fakes; +using NetDaemon.Daemon.Tests.DaemonRunner.App; +using System.Diagnostics.CodeAnalysis; namespace NetDaemon.Daemon.Tests.Daemon { @@ -27,8 +29,7 @@ public async Task EventShouldCallCorrectFunction() // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - - dynamic helloWorldDataObject = GetDynamicDataObject(HelloWorldData); + dynamic helloWorldDataObject = DaemonHostTestBase.GetDynamicDataObject(HelloWorldData); DefaultHassClientMock.AddCustomEvent("CUSTOM_EVENT", helloWorldDataObject); @@ -36,7 +37,7 @@ public async Task EventShouldCallCorrectFunction() var message = ""; // ACT - DefaultDaemonApp.ListenEvent("CUSTOM_EVENT", (ev, data) => + DefaultDaemonApp.ListenEvent("CUSTOM_EVENT", (_, data) => { isCalled = true; message = data.Test; @@ -55,9 +56,10 @@ public async Task AttributeServiceCallShouldFindCorrectFunction() { // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var app = new AssmeblyDaemonApp(); - app.Id = "id"; - + var app = new AssemblyDaemonApp + { + Id = "id" + }; DefaultDaemonHost.InternalRunningAppInstances[app.Id] = app; @@ -106,14 +108,14 @@ public async Task OtherEventShouldNotCallCorrectFunction() { // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - dynamic dataObject = GetDynamicDataObject(); + dynamic dataObject = DaemonHostTestBase.GetDynamicDataObject(); DefaultHassClientMock.AddCustomEvent("CUSTOM_EVENT", dataObject); var isCalled = false; // ACT - DefaultDaemonApp.ListenEvent("OTHER_EVENT", (ev, data) => + DefaultDaemonApp.ListenEvent("OTHER_EVENT", (_, _) => { isCalled = true; return Task.CompletedTask; @@ -132,7 +134,7 @@ public async Task RunNotConnectedCompletesTask() // ACTION var (runTask, _) = ReturnRunningNotConnectedDaemonHostTask(); - await runTask; + await runTask.ConfigureAwait(false); // ASSERT Assert.True(runTask.IsCompleted); @@ -153,11 +155,10 @@ public async Task SendEventShouldCallCorrectMethod() { // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var eventData = GetDynamicDataObject(); - + var eventData = DaemonHostTestBase.GetDynamicDataObject(); // ACT - await DefaultDaemonHost.SendEvent("test_event", eventData); + await DefaultDaemonHost.SendEvent("test_event", eventData).ConfigureAwait(false); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); @@ -172,7 +173,7 @@ public async Task SendEventWithNullDataShouldCallCorrectMethod() // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); // ACT - await DefaultDaemonHost.SendEvent("test_event"); + await DefaultDaemonHost.SendEvent("test_event").ConfigureAwait(false); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); // ASSERT @@ -224,7 +225,7 @@ public async Task SpeakShouldWaitUntilMediaPlays() await InitializeFakeDaemon(500).ConfigureAwait(false); // Expected data call service - var (expectedAttruibutes, expectedAttributesExpObject) = GetDynamicObject( + var (_, expectedAttributesExpObject) = GetDynamicObject( ("entity_id", "media_player.fakeplayer"), ("message", "Hello test!") ); @@ -235,20 +236,19 @@ public async Task SpeakShouldWaitUntilMediaPlays() // ASSERT - await Task.Delay(50); + await Task.Delay(50).ConfigureAwait(false); VerifyCallService("tts", "google_cloud_say", expectedAttributesExpObject, true, Times.Once()); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); // Called twice VerifyCallService("tts", "google_cloud_say", expectedAttributesExpObject, true, Times.Exactly(2)); - } [Fact] public async Task StopCallsCloseClient() { - await DefaultDaemonHost.Stop(); + await DefaultDaemonHost.Stop().ConfigureAwait(false); DefaultHassClientMock.Verify(n => n.CloseAsync(), Times.Once); } @@ -262,7 +262,7 @@ public async Task SubscribeChangedStateForEntityWillMakeCorrectCallback() string? reportedState = ""; // ACT - DefaultDaemonApp.ListenState("binary_sensor.pir", (entityId, newState, oldState) => + DefaultDaemonApp.ListenState("binary_sensor.pir", (_, newState, _) => { reportedState = newState?.State; @@ -285,7 +285,7 @@ public async Task SubscribeChangedStateForAllChangesWillMakeCorrectCallbacks() int nrOfTimesCalled = 0; // ACT - DefaultDaemonApp.ListenState("", (entityId, newState, oldState) => + DefaultDaemonApp.ListenState("", (_, _, _) => { nrOfTimesCalled++; @@ -295,7 +295,7 @@ public async Task SubscribeChangedStateForAllChangesWillMakeCorrectCallbacks() AddChangedEvent("binary_sensor.pir", fromState: "off", toState: "on"); AddChangedEvent("light.mylight", fromState: "on", toState: "off"); - await RunFakeDaemonUntilTimeout(); + await RunFakeDaemonUntilTimeout().ConfigureAwait(false); // ASSERT Assert.Equal(2, nrOfTimesCalled); @@ -327,7 +327,7 @@ public async Task CancelChangedStateForSubscriptionWillNotMakeCallback() bool isCalled = false; // ACT - var id = DefaultDaemonApp.ListenState("binary_sensor.pir", (entityId, newState, oldState) => + var id = DefaultDaemonApp.ListenState("binary_sensor.pir", (_, _, _) => { isCalled = true; @@ -350,8 +350,7 @@ public async Task CallServiceEventShouldCallCorrectFunction() { // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var dynObject = GetDynamicDataObject(HelloWorldData); - + var dynObject = DaemonHostTestBase.GetDynamicDataObject(HelloWorldData); var isCalled = false; string? message = ""; @@ -374,16 +373,17 @@ public async Task CallServiceEventShouldCallCorrectFunction() } [Fact] + [SuppressMessage("", "CA1508")] public async Task CallServiceEventOtherShouldNotCallFunction() { // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var dynObject = GetDynamicDataObject(HelloWorldData); + var dynObject = DaemonHostTestBase.GetDynamicDataObject(HelloWorldData); DefaultHassClientMock.AddCallServiceEvent("custom_domain", "other_service", dynObject); var isCalled = false; - string? message = ""; + string? message = null; DefaultDaemonHost.ListenServiceCall("custom_domain", "any_service", data => { @@ -401,7 +401,7 @@ public async Task CallServiceEventOtherShouldNotCallFunction() [Fact] public async Task SetStateShouldCallCorrectFunction() { - await DefaultDaemonHost.SetStateAsync("sensor.any_sensor", "on", ("attr", "value")); + await DefaultDaemonHost.SetStateAsync("sensor.any_sensor", "on", ("attr", "value")).ConfigureAwait(false); var (dynObj, expObj) = GetDynamicObject( ("attr", "value") @@ -412,7 +412,7 @@ public async Task SetStateShouldCallCorrectFunction() [Fact] public async Task SetStateShouldReturnCorrectData() { - await DefaultDaemonHost.SetStateAsync("sensor.any_sensor", "on", ("attr", "value")); + await DefaultDaemonHost.SetStateAsync("sensor.any_sensor", "on", ("attr", "value")).ConfigureAwait(false); var (dynObj, expObj) = GetDynamicObject( ("attr", "value") @@ -457,7 +457,7 @@ public async Task DelayStateChangeWithToAndFromShouldReturnTrue() [Fact] public async Task DelayStateChangeWithToAndFromWrongShouldNotComplete() { - // ARRANGE + // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); // ACT @@ -478,7 +478,7 @@ public async Task DelayStateLambdaChangeShouldReturnTrue() // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); // ACT - using var delayResult = DefaultDaemonApp.DelayUntilStateChange(new string[] { "binary_sensor.pir" }, (n, o) => n?.State == "on"); + using var delayResult = DefaultDaemonApp.DelayUntilStateChange(new string[] { "binary_sensor.pir" }, (n, _) => n?.State == "on"); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", fromState: "off", toState: "on"); @@ -495,7 +495,7 @@ public async Task DelayStateLambdaChangeShouldNotComplete() // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); // ACT - using var delayResult = DefaultDaemonApp.DelayUntilStateChange(new string[] { "binary_sensor.pir" }, (n, o) => n?.State == "on"); + using var delayResult = DefaultDaemonApp.DelayUntilStateChange(new string[] { "binary_sensor.pir" }, (n, _) => n?.State == "on"); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", fromState: "on", toState: "off"); @@ -551,7 +551,7 @@ public async Task ClearShouldReturnNullGetApp() } [Fact] - public void EntityShouldReturCorrectValueForArea() + public void EntityShouldReturnCorrectValueForArea() { // ARRANGE DefaultDaemonHost._hassDevices["device_id"] = new HassDevice { AreaId = "area_id" }; @@ -569,7 +569,7 @@ public void EntityShouldReturCorrectValueForArea() } [Fact] - public void EntityShouldReturNullForAreaNotExist() + public void EntityShouldReturnNullForAreaNotExist() { // ARRANGE DefaultDaemonHost._hassDevices["device_id"] = new HassDevice { AreaId = "area_id" }; @@ -615,7 +615,7 @@ public async Task SetStateShouldKeepSameArea() // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); // ACT - var state = await DefaultDaemonHost.SetStateAsync("light.ligth_in_area", "on", ("attr", "value")); + var state = await DefaultDaemonHost.SetStateAsync("light.light_in_area", "on", ("attr", "value")).ConfigureAwait(false); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); /// ASSERT Assert.Equal("Area", state?.Area); @@ -634,7 +634,7 @@ public async Task SetStateShouldKeepSameArea() public void FixStateTypesShouldReturnCorrectValues( bool result, dynamic? newState, dynamic? oldState, dynamic? expectedNewState, dynamic? expectedOldState) { - HassStateChangedEventData state = new HassStateChangedEventData + HassStateChangedEventData state = new() { NewState = new HassState { diff --git a/tests/NetDaemon.Daemon.Tests/DaemonRunner/Api/ApiTests.cs b/tests/NetDaemon.Daemon.Tests/DaemonRunner/Api/ApiTests.cs index 4fed71b5e..7597881b5 100644 --- a/tests/NetDaemon.Daemon.Tests/DaemonRunner/Api/ApiTests.cs +++ b/tests/NetDaemon.Daemon.Tests/DaemonRunner/Api/ApiTests.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using JoySoftware.HomeAssistant.Client; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -11,34 +10,35 @@ using NetDaemon.Common; using NetDaemon.Common.Reactive; using NetDaemon.Daemon.Storage; -using NetDaemon.Daemon.Tests.Daemon; using NetDaemon.Service.Api; using Xunit; using System.Threading; -using System.Text.Unicode; using System.Text; using System.Net.WebSockets; using System.IO; using System.Text.Json; -using System.Collections.Generic; using System.Linq; using NetDaemon.Common.Configuration; using NetDaemon.Daemon.Fakes; +using NetDaemon.Common.Exceptions; +using System.Diagnostics.CodeAnalysis; namespace NetDaemon.Daemon.Tests.DaemonRunner.Api { - public class ApiFakeStartup + public class ApiFakeStartup : IAsyncLifetime, IDisposable { + private readonly Mock _defaultMockedRxApp; private readonly Common.NetDaemonApp _defaultDaemonApp; private readonly Common.NetDaemonApp _defaultDaemonApp2; private readonly BaseTestRxApp _defaultDaemonRxApp; - private readonly Mock _defaultMockedRxApp; private readonly NetDaemonHost _defaultDaemonHost; + private readonly HttpHandlerMock _defaultHttpHandlerMock; private readonly LoggerMock _loggerMock; private readonly Mock _defaultDataRepositoryMock; private readonly HassClientMock _defaultHassClientMock; - private readonly HttpHandlerMock _defaultHttpHandlerMock; + private bool disposedValue; + public IConfiguration Configuration { get; } public ApiFakeStartup(IConfiguration configuration) @@ -54,22 +54,28 @@ public ApiFakeStartup(IConfiguration configuration) _loggerMock.LoggerFactory, _defaultHttpHandlerMock.Object); - _defaultDaemonApp = new BaseTestApp(); - _defaultDaemonApp.Id = "app_id"; - _defaultDaemonApp.IsEnabled = true; + _defaultDaemonApp = new BaseTestApp + { + Id = "app_id", + IsEnabled = true + }; _defaultDaemonHost.InternalRunningAppInstances[_defaultDaemonApp.Id!] = _defaultDaemonApp; _defaultDaemonHost.InternalAllAppInstances[_defaultDaemonApp.Id!] = _defaultDaemonApp; - _defaultDaemonApp2 = new BaseTestApp(); - _defaultDaemonApp2.Id = "app_id2"; + _defaultDaemonApp2 = new BaseTestApp + { + Id = "app_id2" + }; _defaultDaemonApp2.RuntimeInfo.NextScheduledEvent = DateTime.Now; _defaultDaemonApp2.IsEnabled = false; _defaultDaemonHost.InternalRunningAppInstances[_defaultDaemonApp2.Id!] = _defaultDaemonApp2; _defaultDaemonHost.InternalAllAppInstances[_defaultDaemonApp2.Id!] = _defaultDaemonApp2; - _defaultDaemonRxApp = new BaseTestRxApp(); - _defaultDaemonRxApp.Id = "app_rx_id"; - _defaultDaemonRxApp.IsEnabled = true; + _defaultDaemonRxApp = new BaseTestRxApp + { + Id = "app_rx_id", + IsEnabled = true + }; _defaultDaemonRxApp.RuntimeInfo.NextScheduledEvent = DateTime.Now; _defaultDaemonHost.InternalRunningAppInstances[_defaultDaemonRxApp.Id!] = _defaultDaemonRxApp; _defaultDaemonHost.InternalAllAppInstances[_defaultDaemonRxApp.Id!] = _defaultDaemonRxApp; @@ -86,16 +92,15 @@ public void ConfigureServices(IServiceCollection services) services.Configure(Configuration.GetSection("HomeAssistant")); services.Configure(Configuration.GetSection("NetDaemon")); - services.AddTransient(n => _defaultHassClientMock.Object); - services.AddTransient(n => _defaultDataRepositoryMock.Object); - services.AddTransient(); - services.AddSingleton(n => _defaultDaemonHost); + services.AddTransient(_ => _defaultHassClientMock.Object); + services.AddTransient(_ => _defaultDataRepositoryMock.Object); + services.AddTransient(); + services.AddSingleton(_ => _defaultDaemonHost); services.AddHttpClient(); } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public static void Configure(IApplicationBuilder app, IWebHostEnvironment _) { - var webSocketOptions = new WebSocketOptions() { KeepAliveInterval = TimeSpan.FromSeconds(120) @@ -105,22 +110,57 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseWebSockets(webSocketOptions); app.UseMiddleware(); } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _defaultHttpHandlerMock.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public Task InitializeAsync() + { + return Task.CompletedTask; + } + + public async Task DisposeAsync() + { + await _defaultDaemonApp.DisposeAsync().ConfigureAwait(false); + await _defaultDaemonApp2.DisposeAsync().ConfigureAwait(false); + await _defaultDaemonRxApp.DisposeAsync().ConfigureAwait(false); + await _defaultDaemonHost.DisposeAsync().ConfigureAwait(false); + } } - public class ApiTests : IAsyncLifetime + + public class ApiTests : IAsyncLifetime, IDisposable { // protected readonly EventQueueManager EventQueueManager; private readonly TestServer _server; - private readonly ArraySegment _buffer; - - private JsonSerializerOptions _jsonOptions = new JsonSerializerOptions + private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private bool disposedValue; + + public ArraySegment Buffer { get; } public ApiTests() { - _buffer = new ArraySegment(new byte[8192]); + Buffer = new ArraySegment(new byte[8192]); var builder = WebHost.CreateDefaultBuilder() .UseEnvironment("Testing") @@ -139,32 +179,29 @@ public Task InitializeAsync() return Task.CompletedTask; } - private async Task ReadString(WebSocket ws) + [SuppressMessage("", "CA2201")] + private static async Task ReadString(WebSocket ws) { var buffer = new ArraySegment(new byte[8192]); - _ = buffer.Array ?? throw new NullReferenceException("Failed to allocate memory buffer"); + _ = buffer.Array ?? throw new NetDaemonNullReferenceException("Failed to allocate memory buffer"); - using (var ms = new MemoryStream()) + using var ms = new MemoryStream(); + WebSocketReceiveResult result; + do { - WebSocketReceiveResult result; - do - { - result = await ws.ReceiveAsync(buffer, CancellationToken.None); - ms.Write(buffer.Array, buffer.Offset, result.Count); - } - while (!result.EndOfMessage); - - ms.Seek(0, SeekOrigin.Begin); - if (result.MessageType != WebSocketMessageType.Text) - { - throw new Exception("Unexpected type"); - } + result = await ws.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false); + ms.Write(buffer.Array, buffer.Offset, result.Count); + } + while (!result.EndOfMessage); - using (var reader = new StreamReader(ms, Encoding.UTF8)) - { - return await reader.ReadToEndAsync(); - } + ms.Seek(0, SeekOrigin.Begin); + if (result.MessageType != WebSocketMessageType.Text) + { + throw new Exception("Unexpected type"); } + + using var reader = new StreamReader(ms, Encoding.UTF8); + return await reader.ReadToEndAsync().ConfigureAwait(false); } private async Task GetWsClient() @@ -175,21 +212,20 @@ private async Task GetWsClient() private async Task ReadObject(WebSocket ws, Type t) { - var s = await ReadString(ws); + var s = await ReadString(ws).ConfigureAwait(false); return JsonSerializer.Deserialize(s, t, _jsonOptions); } [Fact] public async Task TestGetApps() { + var websocket = await GetWsClient().ConfigureAwait(false); - var websocket = await GetWsClient(); + await websocket.SendAsync(Encoding.UTF8.GetBytes(@"{""type"": ""apps""}"), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false); - await websocket.SendAsync(Encoding.UTF8.GetBytes(@"{""type"": ""apps""}"), WebSocketMessageType.Text, true, CancellationToken.None); - - var res = (WsAppsResult?)await ReadObject(websocket, typeof(WsAppsResult)); + var res = (WsAppsResult?)await ReadObject(websocket, typeof(WsAppsResult)).ConfigureAwait(false); Assert.NotNull(res); - var response = res?.Data!; + var response = (res?.Data)!; Assert.Equal(4, response?.Count()); @@ -213,18 +249,37 @@ public async Task TestGetApps() [Fact] public async Task TestGetConfig() { - var websocket = await GetWsClient(); + var websocket = await GetWsClient().ConfigureAwait(false); - await websocket.SendAsync(Encoding.UTF8.GetBytes(@"{""type"": ""settings""}"), WebSocketMessageType.Text, true, CancellationToken.None); + await websocket.SendAsync(Encoding.UTF8.GetBytes(@"{""type"": ""settings""}"), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false); - var res = (WsConfigResult?)await ReadObject(websocket, typeof(WsConfigResult)); + var res = (WsConfigResult?)await ReadObject(websocket, typeof(WsConfigResult)).ConfigureAwait(false); Assert.NotNull(res); var response = res?.Data; Assert.NotNull(response); Assert.NotNull(response?.DaemonSettings?.AppSource); + } + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _server.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); } } } \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/AssemblyDaemonApps.cs b/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/AssemblyDaemonApps.cs index 3e70f194d..b0ea544ed 100644 --- a/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/AssemblyDaemonApps.cs +++ b/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/AssemblyDaemonApps.cs @@ -2,43 +2,46 @@ using System.Threading.Tasks; using NetDaemon.Common; -/// -/// Greets (or insults) people when coming home :) -/// -public class AssmeblyDaemonApp : NetDaemonApp +namespace NetDaemon.Daemon.Tests.DaemonRunner.App { - #region -- Test config -- + /// + /// Greets (or insults) people when coming home :) + /// + public class AssemblyDaemonApp : Common.NetDaemonApp + { + #region -- Test config -- - public string? StringConfig { get; set; } = null; - public int? IntConfig { get; set; } = null; - public IEnumerable? EnumerableConfig { get; set; } = null; + public string? StringConfig { get; set; } + public int? IntConfig { get; set; } + public IEnumerable? EnumerableConfig { get; set; } - #endregion -- Test config -- + #endregion -- Test config -- - #region -- Test secrets -- + #region -- Test secrets -- - public string? TestSecretString { get; set; } - public int? TestSecretInt { get; set; } + public string? TestSecretString { get; set; } + public int? TestSecretInt { get; set; } - public string? TestNormalString { get; set; } - public int? TestNormalInt { get; set; } + public string? TestNormalString { get; set; } + public int? TestNormalInt { get; set; } - #endregion -- Test secrets -- + #endregion -- Test secrets -- - // For testing - public bool HandleServiceCallIsCalled { get; set; } = false; + // For testing + public bool HandleServiceCallIsCalled { get; set; } - public override Task InitializeAsync() - { - // Do nothing + public override Task InitializeAsync() + { + // Do nothing - return Task.CompletedTask; - } + return Task.CompletedTask; + } - [HomeAssistantServiceCall] - public Task HandleServiceCall(dynamic data) - { - HandleServiceCallIsCalled = true; - return Task.CompletedTask; + [HomeAssistantServiceCall] + public Task HandleServiceCall(dynamic _) + { + HandleServiceCallIsCalled = true; + return Task.CompletedTask; + } } } \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/CodeGenTests.cs b/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/CodeGenTests.cs index f2fb5f295..53c51ebd0 100644 --- a/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/CodeGenTests.cs +++ b/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/CodeGenTests.cs @@ -10,9 +10,8 @@ public class CodeGenerationTests public void TestGenerateCode() { // ARRANGE - var x = new CodeGenerator(); // ACT - var code = x.GenerateCode("Netdaemon.Generated.Extensions", new string[] { "light.koket_fonster", "media_player.my_player" }); + var code = CodeGenerator.GenerateCode("Netdaemon.Generated.Extensions", new string[] { "light.koket_fonster", "media_player.my_player" }); // ASSERT diff --git a/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/DaemonAppTests.cs b/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/DaemonAppTests.cs index 03434c17b..7f29692a1 100644 --- a/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/DaemonAppTests.cs +++ b/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/DaemonAppTests.cs @@ -24,16 +24,15 @@ public class AppTests public static readonly string FaultyAppPath = Path.Combine(AppContext.BaseDirectory, "DaemonRunner", "FaultyApp"); - public string GetFixtureContent(string filename) => File.ReadAllText(Path.Combine(AppTests.ConfigFixturePath, filename)); + public static string GetFixtureContent(string filename) => File.ReadAllText(Path.Combine(AppTests.ConfigFixturePath, filename)); - public string GetFixturePath(string filename) => Path.Combine(AppTests.ConfigFixturePath, filename); + public static string GetFixturePath(string filename) => Path.Combine(AppTests.ConfigFixturePath, filename); - public IOptions CreateSettings(string appSource) => new OptionsWrapper(new NetDaemonSettings + public static IOptions CreateSettings(string appSource) => new OptionsWrapper(new NetDaemonSettings { AppSource = appSource }); - [Fact] public void FaultyApplicationShouldLogError() { @@ -44,7 +43,7 @@ public void FaultyApplicationShouldLogError() var netDaemonSettings = CreateSettings(path); // ACT - var cm = new CodeManager(daemonApps, loggerMock.Logger, new YamlConfig(netDaemonSettings)); + _ = new CodeManager(daemonApps, loggerMock.Logger, new YamlConfig(netDaemonSettings)); // ASSERT loggerMock.AssertLogged(LogLevel.Error, Times.AtLeastOnce()); @@ -90,8 +89,8 @@ public void InstanceAppFromConfigShouldReturnCorrectType() yamlConfigMock.Setup(x => x.GetAllConfigFilePaths()) .Returns(new[] { Path.Combine(ConfigFixturePath, "level2", "level3") }); - IEnumerable types = new List() { typeof(AssmeblyDaemonApp) }; - var yamlConfig = "app:\n class: AssmeblyDaemonApp"; + IEnumerable types = new List() { typeof(AssemblyDaemonApp) }; + const string? yamlConfig = "app:\n class: NetDaemon.Daemon.Tests.DaemonRunner.App.AssemblyDaemonApp"; // ACT var instances = new YamlAppConfig(types, new StringReader(yamlConfig), yamlConfigMock.Object, "").Instances; // ASSERT @@ -109,8 +108,8 @@ public void InstanceAppFromConfigNotFoundShouldReturnNull() yamlConfigMock.Setup(x => x.GetAllConfigFilePaths()) .Returns(new[] { Path.Combine(ConfigFixturePath, "level2", "level3") }); - IEnumerable types = new List() { typeof(AssmeblyDaemonApp) }; - var yamlConfig = "app:\n class: NotFoundApp"; + IEnumerable types = new List() { typeof(AssemblyDaemonApp) }; + const string? yamlConfig = "app:\n class: NotFoundApp"; // ACT var instances = new YamlAppConfig(types, new StringReader(yamlConfig), yamlConfigMock.Object, "").Instances; @@ -127,10 +126,10 @@ public void InstanceAppFromConfigShouldHaveCorrectProperties() yamlConfigMock.Setup(x => x.GetAllConfigFilePaths()) .Returns(new[] { Path.Combine(ConfigFixturePath, "level2", "level3") }); - IEnumerable types = new List() { typeof(AssmeblyDaemonApp) }; - var yamlConfig = @" + IEnumerable types = new List() { typeof(AssemblyDaemonApp) }; + const string? yamlConfig = @" app: - class: AssmeblyDaemonApp + class: NetDaemon.Daemon.Tests.DaemonRunner.App.AssemblyDaemonApp StringConfig: a string IntConfig: 10 EnumerableConfig: @@ -139,7 +138,7 @@ public void InstanceAppFromConfigShouldHaveCorrectProperties() "; // ACT var instances = new YamlAppConfig(types, new StringReader(yamlConfig), yamlConfigMock.Object, "").Instances; - var instance = instances.FirstOrDefault() as AssmeblyDaemonApp; + var instance = instances.FirstOrDefault() as AssemblyDaemonApp; // ASSERT Assert.Equal("a string", instance?.StringConfig); Assert.Equal(10, instance?.IntConfig); @@ -152,10 +151,10 @@ public void InstanceAppFromConfigWithSecretsShouldHaveCorrectProperties() // ARRANGE var config = new YamlConfig(CreateSettings(Path.Combine(ConfigFixturePath, "level2", "level3"))); - IEnumerable types = new List() { typeof(AssmeblyDaemonApp) }; - var yamlConfig = @" + IEnumerable types = new List() { typeof(AssemblyDaemonApp) }; + const string? yamlConfig = @" app: - class: AssmeblyDaemonApp + class: NetDaemon.Daemon.Tests.DaemonRunner.App.AssemblyDaemonApp test_secret_string: !secret a_secret_string test_secret_int: !secret a_secret_int test_normal_string: not a secret string @@ -168,7 +167,7 @@ public void InstanceAppFromConfigWithSecretsShouldHaveCorrectProperties() config, Path.Combine(ConfigFixturePath, "level2", "level3", "any.cs") ).Instances; - var instance = instances.FirstOrDefault() as AssmeblyDaemonApp; + var instance = instances.FirstOrDefault() as AssemblyDaemonApp; // ASSERT Assert.Equal("this is a secret string", instance?.TestSecretString); Assert.Equal(99, instance?.TestSecretInt); @@ -183,10 +182,10 @@ public void InstanceAppFromConfigShouldHaveCorrectPropertiesCamelCaseConvert() yamlConfigMock.Setup(x => x.GetAllConfigFilePaths()) .Returns(new[] { Path.Combine(ConfigFixturePath, "level2", "level3") }); - IEnumerable types = new List() { typeof(AssmeblyDaemonApp) }; - var yamlConfig = @" + IEnumerable types = new List() { typeof(AssemblyDaemonApp) }; + const string? yamlConfig = @" app: - class: AssmeblyDaemonApp + class: NetDaemon.Daemon.Tests.DaemonRunner.App.AssemblyDaemonApp string_config: a string int_config: 10 enumerable_config: @@ -195,7 +194,7 @@ public void InstanceAppFromConfigShouldHaveCorrectPropertiesCamelCaseConvert() "; // ACT var instances = new YamlAppConfig(types, new StringReader(yamlConfig), yamlConfigMock.Object, "").Instances; - var instance = instances.FirstOrDefault() as AssmeblyDaemonApp; + var instance = instances.FirstOrDefault() as AssemblyDaemonApp; // ASSERT Assert.Equal("a string", instance?.StringConfig); Assert.Equal(10, instance?.IntConfig); @@ -244,29 +243,27 @@ public async Task StorageShouldReturnSameValueAsSet(object data) yamlConfigMock.Setup(x => x.GetAllConfigFilePaths()) .Returns(new[] { Path.Combine(ConfigFixturePath, "level2", "level3") }); - IEnumerable types = new List() { typeof(AssmeblyDaemonApp) }; - var yamlConfig = @" + IEnumerable types = new List() { typeof(AssemblyDaemonApp) }; + const string? yamlConfig = @" app: - class: AssmeblyDaemonApp + class: NetDaemon.Daemon.Tests.DaemonRunner.App.AssemblyDaemonApp "; var daemonMock = new Mock(); daemonMock.SetupGet(x => x.Logger).Returns(new Mock().Object); - await using var instance = new YamlAppConfig(types, new StringReader(yamlConfig), yamlConfigMock.Object, "").Instances.FirstOrDefault() as AssmeblyDaemonApp; - + await using var instance = new YamlAppConfig(types, new StringReader(yamlConfig), yamlConfigMock.Object, "").Instances.FirstOrDefault() as AssemblyDaemonApp; instance!.Id = "somefake_id"; instance.InternalStorageObject = new FluentExpandoObject(false, true, daemon: instance); instance.Logger = new Mock().Object; - // await instance!.StartUpAsync(daemonMock.Object); // ACT instance!.Storage.Data = data; // ASSERT Assert.Equal(data, instance.Storage.Data); - var stateQueueResult = await instance.InternalLazyStoreStateQueue.Reader.WaitToReadAsync(); + var stateQueueResult = await instance.InternalLazyStoreStateQueue.Reader.WaitToReadAsync().ConfigureAwait(false); Assert.True(stateQueueResult); } @@ -279,93 +276,45 @@ public async Task StorageShouldRestoreWithCorrectValues() yamlConfigMock.Setup(x => x.GetAllConfigFilePaths()) .Returns(new[] { Path.Combine(ConfigFixturePath, "level2", "level3") }); - IEnumerable types = new List() { typeof(AssmeblyDaemonApp) }; - var yamlConfig = @" + IEnumerable types = new List() { typeof(AssemblyDaemonApp) }; + const string? yamlConfig = @" app: - class: AssmeblyDaemonApp + class: NetDaemon.Daemon.Tests.DaemonRunner.App.AssemblyDaemonApp "; - await using var instance = new YamlAppConfig(types, new StringReader(yamlConfig), yamlConfigMock.Object, "").Instances.FirstOrDefault() as AssmeblyDaemonApp; + await using var instance = new YamlAppConfig(types, new StringReader(yamlConfig), yamlConfigMock.Object, "").Instances.FirstOrDefault() as AssemblyDaemonApp; var daemonMock = new Mock(); daemonMock.SetupGet(x => x.Logger).Returns(new Mock().Object); - var storageItem = new FluentExpandoObject(); - storageItem["Data"] = "SomeData"; + var storageItem = new FluentExpandoObject + { + ["Data"] = "SomeData" + }; daemonMock.SetupGet(x => x.Logger).Returns(new Mock().Object); daemonMock.Setup(n => n.GetDataAsync>(It.IsAny())) .ReturnsAsync((IDictionary)storageItem); - await instance!.StartUpAsync(daemonMock.Object); + await instance!.StartUpAsync(daemonMock.Object).ConfigureAwait(false); // ACT - await instance.RestoreAppStateAsync(); + await instance.RestoreAppStateAsync().ConfigureAwait(false); // ASSERT Assert.Equal("SomeData", instance.Storage.Data); } - // Todo: Make test to test sorting in NetDaemon instead - // [Fact] - // public void InstanceAppFromConfigFilesInFolderWithDependenciesShouldReturnCorrectInstances() - // { - // // ARRANGE - // var path = Path.Combine(ConfigFixturePath, "dependtests"); - // var moqDaemon = new Mock(); - // var moqLogger = new LoggerMock(); - - // moqDaemon.SetupGet(n => n.Logger).Returns(moqLogger.Logger); - // // ACT - // var codeManager = CM(path); - // // ASSERT - // var instances = codeManager.InstanceDaemonApps(); - - // // Todo: refactor this, only had to make test work after refactor - // var realDaemon = new NetDaemonHost(new Mock().Object, new Mock().Object, null, null); - // instances = realDaemon.SortByDependency(instances); - - // Assert.Collection(instances, - // i => Assert.Equal("app_global", i.Id), - // i => Assert.Equal("app_dep_on_global", i.Id), - // i => Assert.Equal("app_dep_on_global_and_other", i.Id), - // i => Assert.Equal("app_dep_app_depend_on_global_and_other", i.Id) - // ); - - // // moqDaemon.Verify(n => n.RegisterAppInstance("app_global", It.IsAny())); - // // moqDaemon.Verify(n => n.RegisterAppInstance("app_dep_on_global", It.IsAny())); - // // moqDaemon.Verify(n => n.RegisterAppInstance("app_dep_on_global_and_other", It.IsAny())); - // // moqDaemon.Verify(n => n.RegisterAppInstance("app_dep_app_depend_on_global_and_other", It.IsAny())); - // } - - //FaultyAppPath - // [Fact] - // public async Task InstanceAppsThatHasCircularDependenciesShouldReturnNull() - // { - // // ARRANGE - // var path = Path.Combine(FaultyAppPath, "CircularDependencies"); - // var moqDaemon = new Mock(); - // var moqLogger = new LoggerMock(); - - // moqDaemon.SetupGet(n => n.Logger).Returns(moqLogger.Logger); - // var codeManager = CM(path); - // // ACT - // // ASSERT - // var ex = await Assert.Throws( () => { codeManager.InstanceDaemonApps(); }); - // Assert.Contains("Application dependencies is wrong", ex.Message); - // } - [Fact] public void InsanceAppsThatHasMissingExecuteShouldLogError() { // ARRANGE var path = Path.Combine(FaultyAppPath, "ExecuteWarnings"); - var moqDaemon = new Mock(); var moqLogger = new LoggerMock(); var (daemonApps, _) = DaemonCompiler.GetDaemonApps(path, moqLogger.Logger); var configMock = new Mock(); // ACT - var codeManager = new CodeManager(daemonApps, moqLogger.Logger, configMock.Object); + _ = new CodeManager(daemonApps, moqLogger.Logger, configMock.Object); // ASSERT moqLogger.AssertLogged(LogLevel.Error, Times.Exactly(13)); @@ -376,19 +325,18 @@ public void InsanceAppsThatHasMissingExecuteAndSupressLogsShouldNotLogError() { // ARRANGE var path = Path.Combine(FaultyAppPath, "SupressLogs"); - var moqDaemon = new Mock(); var moqLogger = new LoggerMock(); var (daemonApps, _) = DaemonCompiler.GetDaemonApps(path, moqLogger.Logger); var configMock = new Mock(); // ACT - var codeManager = new CodeManager(daemonApps, moqLogger.Logger, configMock.Object); + _ = new CodeManager(daemonApps, moqLogger.Logger, configMock.Object); // ASSERT moqLogger.AssertLogged(LogLevel.Error, Times.Exactly(1)); } - public CodeManager CM(string path) + public static CodeManager CM(string path) { var loggerMock = new LoggerMock().Logger; var config = new YamlConfig(CreateSettings(path)); diff --git a/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/AppComplexConfig.cs b/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/AppComplexConfig.cs index 4103d362f..420f578d2 100644 --- a/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/AppComplexConfig.cs +++ b/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/AppComplexConfig.cs @@ -23,12 +23,12 @@ public override Task InitializeAsync() public class Device { - public string? name { get; set; } - public IEnumerable? commands { get; set; } + public string? Name { get; set; } + public IEnumerable? Commands { get; set; } } public class Command { - public string? name { get; set; } - public string? data { get; set; } + public string? Name { get; set; } + public string? Data { get; set; } } } \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/ConfigTest.cs b/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/ConfigTest.cs index e23653452..542ad2cd5 100644 --- a/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/ConfigTest.cs +++ b/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/ConfigTest.cs @@ -16,11 +16,11 @@ public class YamlTests public static readonly string ConfigFixturePath = Path.Combine(AppContext.BaseDirectory, "DaemonRunner", "Fixtures"); - public string GetFixtureContent(string filename) => File.ReadAllText(Path.Combine(YamlTests.ConfigFixturePath, filename)); + public static string GetFixtureContent(string filename) => File.ReadAllText(Path.Combine(YamlTests.ConfigFixturePath, filename)); - public string GetFixturePath(string filename) => Path.Combine(YamlTests.ConfigFixturePath, filename); + public static string GetFixturePath(string filename) => Path.Combine(YamlTests.ConfigFixturePath, filename); - public IOptions CreateSettings(string appSource) => new OptionsWrapper(new NetDaemonSettings + public static IOptions CreateSettings(string appSource) => new OptionsWrapper(new NetDaemonSettings { AppSource = appSource }); @@ -46,7 +46,7 @@ public void NormalLoadSecretsShouldGetCorrectValues() public void SecretsLoadFaultyYamlThrowsException() { // ARRANGE - var faultyYaml = + const string? faultyYaml = "yaml: correctLine\n" + "yaml_missing: \"missing" + "yaml_correct: 10"; @@ -102,14 +102,13 @@ public void SecretShouldBeRelevantDependingOnFolderLevel(string secret, string c public void YamlScalarNodeToObjectUsingString() { // ARRANGE - var yaml = "yaml: string\n"; + const string? yaml = "yaml: string\n"; var yamlStream = new YamlStream(); yamlStream.Load(new StringReader(yaml)); var root = (YamlMappingNode)yamlStream.Documents[0].RootNode; var scalar = root.Children.First(); - string? map = ((YamlScalarNode)scalar.Key).Value; var scalarValue = (YamlScalarNode)scalar.Value; // ACT & ASSERT Assert.Equal("string", scalarValue.ToObject(typeof(string))); @@ -119,14 +118,13 @@ public void YamlScalarNodeToObjectUsingString() public void YamlScalarNodeToObjectUsingInt() { // ARRANGE - var yaml = "yaml: 1234\n"; + const string? yaml = "yaml: 1234\n"; var yamlStream = new YamlStream(); yamlStream.Load(new StringReader(yaml)); var root = (YamlMappingNode)yamlStream.Documents[0].RootNode; var scalar = root.Children.First(); - string? map = ((YamlScalarNode)scalar.Key).Value; var scalarValue = (YamlScalarNode)scalar.Value; // ACT & ASSERT Assert.Equal(1234, scalarValue.ToObject(typeof(int))); @@ -136,14 +134,13 @@ public void YamlScalarNodeToObjectUsingInt() public void YamlScalarNodeToObjectUsingBoolean() { // ARRANGE - var yaml = "yaml: true\n"; + const string? yaml = "yaml: true\n"; var yamlStream = new YamlStream(); yamlStream.Load(new StringReader(yaml)); var root = (YamlMappingNode)yamlStream.Documents[0].RootNode; var scalar = root.Children.First(); - string? map = ((YamlScalarNode)scalar.Key).Value; var scalarValue = (YamlScalarNode)scalar.Value; // ACT & ASSERT Assert.Equal(true, scalarValue.ToObject(typeof(bool))); @@ -153,31 +150,29 @@ public void YamlScalarNodeToObjectUsingBoolean() public void YamlScalarNodeToObjectUsingLong() { // ARRANGE - var yaml = "yaml: 1234\n"; + const string? yaml = "yaml: 1234\n"; var yamlStream = new YamlStream(); yamlStream.Load(new StringReader(yaml)); var root = (YamlMappingNode)yamlStream.Documents[0].RootNode; var scalar = root.Children.First(); - string? map = ((YamlScalarNode)scalar.Key).Value; var scalarValue = (YamlScalarNode)scalar.Value; // ACT & ASSERT Assert.Equal((long)1234, scalarValue.ToObject(typeof(long))); } [Fact] - public void YamlScalarNodeToObjectUsingDeciaml() + public void YamlScalarNodeToObjectUsingDecimal() { // ARRANGE - var yaml = "yaml: 1.5\n"; + const string? yaml = "yaml: 1.5\n"; var yamlStream = new YamlStream(); yamlStream.Load(new StringReader(yaml)); var root = (YamlMappingNode)yamlStream.Documents[0].RootNode; var scalar = root.Children.First(); - string? map = ((YamlScalarNode)scalar.Key).Value; var scalarValue = (YamlScalarNode)scalar.Value; // ACT & ASSERT Assert.Equal((decimal)1.5, scalarValue.ToObject(typeof(decimal))); @@ -188,7 +183,7 @@ public void YamlScalarNodeToObjectUsingDeciaml() [Fact] public void YamlAdvancedObjectsShouldReturnCorrectData() { - var yaml = @" + const string? yaml = @" a_string: hello world an_int: 10 a_bool: true @@ -223,11 +218,11 @@ public void YamlAdvancedObjectsShouldReturnCorrectData() Assert.Equal(true, instance?.ABool); Assert.NotNull(instance?.Devices); Assert.Equal(1, instance?.Devices?.Count()); - Assert.Equal("tv", instance?.Devices?.First().name); - Assert.Equal("command1", instance?.Devices?.First()?.commands?.ElementAt(0).name); - Assert.Equal("some code", instance?.Devices?.First()?.commands?.ElementAt(0).data); - Assert.Equal("command2", instance?.Devices?.First()?.commands?.ElementAt(1).name); - Assert.Equal("some code2", instance?.Devices?.First()?.commands?.ElementAt(1).data); + Assert.Equal("tv", instance?.Devices?.First().Name); + Assert.Equal("command1", instance?.Devices?.First()?.Commands?.ElementAt(0).Name); + Assert.Equal("some code", instance?.Devices?.First()?.Commands?.ElementAt(0).Data); + Assert.Equal("command2", instance?.Devices?.First()?.Commands?.ElementAt(1).Name); + Assert.Equal("some code2", instance?.Devices?.First()?.Commands?.ElementAt(1).Data); } } } \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/DaemonRunner/FaultyApp/CompileErrors/AssemblyDaemonApps.cs b/tests/NetDaemon.Daemon.Tests/DaemonRunner/FaultyApp/CompileErrors/AssemblyDaemonApps.cs index b166c4194..f963bb85a 100644 --- a/tests/NetDaemon.Daemon.Tests/DaemonRunner/FaultyApp/CompileErrors/AssemblyDaemonApps.cs +++ b/tests/NetDaemon.Daemon.Tests/DaemonRunner/FaultyApp/CompileErrors/AssemblyDaemonApps.cs @@ -5,6 +5,8 @@ /// /// Greets (or insults) people when coming home :) /// +[SuppressMessage("", "CS1002")] +[SuppressMessage("", "CS1038")] public class AssmeblyFaultyCompileErrorDaemonApp : NetDaemonApp { #region -- Test config -- diff --git a/tests/NetDaemon.Daemon.Tests/FakesTests/FakeApp.cs b/tests/NetDaemon.Daemon.Tests/FakesTests/FakeApp.cs index d96d5c21f..cb65eb10f 100644 --- a/tests/NetDaemon.Daemon.Tests/FakesTests/FakeApp.cs +++ b/tests/NetDaemon.Daemon.Tests/FakesTests/FakeApp.cs @@ -5,7 +5,6 @@ namespace NetDaemon.Daemon.Tests.Reactive { - /// cool multiple lines public class FakeApp : NetDaemonRxApp { @@ -14,18 +13,12 @@ public override void Initialize() Entity("binary_sensor.kitchen") .StateChanges .Where(e => e.New?.State == "on" && e.Old?.State == "off") - .Subscribe(s => - { - Entity("light.kitchen").TurnOn(); - }); + .Subscribe(_ => Entity("light.kitchen").TurnOn()); Entity("sensor.temperature") .StateAllChanges .Where(e => e.New?.Attribute?.battery_level < 15) - .Subscribe(s => - { - this.CallService("notify", "notify", new { title = "Hello from Home Assistant" }); - }); + .Subscribe(_ => this.CallService("notify", "notify", new { title = "Hello from Home Assistant" })); } } } diff --git a/tests/NetDaemon.Daemon.Tests/FakesTests/FakeTests.cs b/tests/NetDaemon.Daemon.Tests/FakesTests/FakeTests.cs index 4a1a8a273..db7a2ccdd 100644 --- a/tests/NetDaemon.Daemon.Tests/FakesTests/FakeTests.cs +++ b/tests/NetDaemon.Daemon.Tests/FakesTests/FakeTests.cs @@ -1,10 +1,12 @@ using System; using System.Dynamic; +using System.Globalization; using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using JoySoftware.HomeAssistant.Client; using Moq; +using NetDaemon.Common.Exceptions; using NetDaemon.Common.Fluent; using NetDaemon.Common.Reactive; using NetDaemon.Daemon.Fakes; @@ -30,7 +32,7 @@ public async Task CallServiceShouldCallCorrectFunction() { // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var (dynObj, expObj) = GetDynamicObject( + var (dynObj, _) = GetDynamicObject( ("attr", "value")); // ACT @@ -50,10 +52,7 @@ public async Task NewAllEventDataShouldCallFunction() // ACT DefaultDaemonRxApp.StateAllChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "on", "on"); @@ -72,10 +71,7 @@ public async Task NewEventShouldCallFunction() // ACT DefaultDaemonRxApp.EventChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddCustomEvent("AN_EVENT", new { somedata = "hello" }); @@ -94,10 +90,7 @@ public async Task NewEventMissingDataAttributeShouldReturnNull() // ACT DefaultDaemonRxApp.EventChanges - .Subscribe(s => - { - missingAttribute = s.Data?.missing_data; - }); + .Subscribe(s => missingAttribute = s.Data?.missing_data); var expandoObj = new ExpandoObject(); dynamic dynExpObject = expandoObj; @@ -120,10 +113,7 @@ public async Task NewStateEventShouldCallFunction() // ACT DefaultDaemonRxApp.StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); @@ -139,9 +129,6 @@ public async Task RunScriptShouldCallCorrectFunction() // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var (dynObj, expObj) = GetDynamicObject( - ("attr", "value")); - // ACT DefaultDaemonRxApp.RunScript("myscript"); @@ -149,7 +136,6 @@ public async Task RunScriptShouldCallCorrectFunction() // ASSERT - DefaultHassClientMock.VerifyCallServiceTimes("myscript", Times.Once()); } @@ -158,8 +144,6 @@ public async Task RunScriptWithDomainShouldCallCorrectFunction() { // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var (dynObj, expObj) = GetDynamicObject( - ("attr", "value")); // ACT DefaultDaemonRxApp.RunScript("script.myscript"); @@ -178,10 +162,7 @@ public async Task SameStateEventShouldNotCallFunction() // ACT DefaultDaemonRxApp.StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "on", "on"); @@ -213,7 +194,7 @@ public async Task StartupAsyncShouldThrowIfDaemonIsNull() INetDaemonHost? host = null; // ARRANGE ACT ASSERT - await Assert.ThrowsAsync(() => DefaultDaemonRxApp.StartUpAsync(host!)); + await Assert.ThrowsAsync(() => DefaultDaemonRxApp.StartUpAsync(host!)).ConfigureAwait(false); } [Fact] @@ -271,7 +252,7 @@ public async Task EntityIdsShouldReturnCorrectItems() await RunFakeDaemonUntilTimeout().ConfigureAwait(false); // ASSERT Assert.NotNull(entities); - Assert.Equal(8, entities.Count()); + Assert.Equal(8, entities.Count); } [Fact] @@ -282,12 +263,9 @@ public async Task UsingEntitiesLambdaNewEventShouldCallFunction() var called = false; // ACT - DefaultDaemonRxApp.Entities(n => n.EntityId.StartsWith("binary_sensor.pir")) + DefaultDaemonRxApp.Entities(n => n.EntityId.StartsWith("binary_sensor.pir", true, CultureInfo.InvariantCulture)) .StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir_2", "off", "on"); @@ -306,12 +284,9 @@ public async Task CallbackObserverAttributeMissingShouldReturnNull() string? missingString = "has initial value"; // ACT - DefaultDaemonRxApp.Entities(n => n.EntityId.StartsWith("binary_sensor.pir")) + DefaultDaemonRxApp.Entities(n => n.EntityId.StartsWith("binary_sensor.pir", true, CultureInfo.InvariantCulture)) .StateChanges - .Subscribe(s => - { - missingString = s.New.Attribute?.missing_attribute; - }); + .Subscribe(s => missingString = s.New.Attribute?.missing_attribute); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir_2", "off", "on"); @@ -331,10 +306,7 @@ public async Task UsingEntitiesNewEventShouldCallFunction() // ACT DefaultDaemonRxApp.Entities("binary_sensor.pir", "binary_sensor.pir_2") .StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir_2", "off", "on"); @@ -354,10 +326,7 @@ public async Task UsingEntityNewEventShouldCallFunction() // ACT DefaultDaemonRxApp.Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); @@ -377,10 +346,7 @@ public async Task UsingEntityNewEventShouldNotCallFunction() // ACT DefaultDaemonRxApp.Entity("binary_sensor.other_pir") .StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); @@ -392,16 +358,13 @@ public async Task UsingEntityNewEventShouldNotCallFunction() [Fact] public async Task WhenStateStaysSameForTimeItShouldCallFunction() { - await InitializeFakeDaemon().ConfigureAwait(false); + await InitializeFakeDaemon(100).ConfigureAwait(false); bool isRun = false; using var ctx = DefaultDaemonRxApp.StateChanges .Where(t => t.New.EntityId == "binary_sensor.pir") .NDSameStateFor(TimeSpan.FromMilliseconds(50)) - .Subscribe(e => - { - isRun = true; - }); + .Subscribe(_ => isRun = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); @@ -449,7 +412,8 @@ public async Task GetDataShouldReturnCachedValue() public async Task TestFakeAppTurnOnCorrectLight() { // Add the app to test - await AddAppInstance(new FakeApp()); + await using var fakeApp = new FakeApp(); + await AddAppInstance(fakeApp).ConfigureAwait(false); // Init NetDaemon core runtime await InitializeFakeDaemon().ConfigureAwait(false); @@ -460,16 +424,16 @@ public async Task TestFakeAppTurnOnCorrectLight() // Run the NetDemon Core to process the messages await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // Verify that netdaemon called light.turn_on + // Verify that netdaemon called light.turn_on VerifyCallService("light", "turn_on", "light.kitchen"); } - [Fact] public async Task TestFakeAppCallNoteWhenBatteryLevelBelowValue() { // Add the app to test - await AddAppInstance(new FakeApp()); + await using var fakeApp = new FakeApp(); + await AddAppInstance(fakeApp).ConfigureAwait(false); // Init NetDaemon core runtime await InitializeFakeDaemon().ConfigureAwait(false); @@ -497,7 +461,7 @@ public async Task TestFakeAppCallNoteWhenBatteryLevelBelowValue() // Run the NetDemon Core to process the messages await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // Verify that netdaemon called light.turn_on + // Verify that netdaemon called light.turn_on VerifyCallService("notify", "notify", new { title = "Hello from Home Assistant" }); } } diff --git a/tests/NetDaemon.Daemon.Tests/Fluent/FluentCameraTests.cs b/tests/NetDaemon.Daemon.Tests/Fluent/FluentCameraTests.cs index e187702bf..1b0e4934a 100644 --- a/tests/NetDaemon.Daemon.Tests/Fluent/FluentCameraTests.cs +++ b/tests/NetDaemon.Daemon.Tests/Fluent/FluentCameraTests.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Moq; @@ -25,14 +26,15 @@ public FluentCameraTests() : base() public async Task CameraDisableMotionDetectionCallsCorrectServiceCall() { // ARRANGE - var entityId = "camera.camera1"; - var service_call = "disable_motion_detection"; + const string? entityId = "camera.camera1"; + const string? service_call = "disable_motion_detection"; // ACT await DefaultDaemonApp .Camera(entityId) .DisableMotionDetection() - .ExecuteAsync(); + .ExecuteAsync() + .ConfigureAwait(false); // ASSERT DefaultHassClientMock.VerifyCallServiceTimes(service_call, Times.Once()); @@ -43,14 +45,15 @@ await DefaultDaemonApp public async Task CamerasDisableMotionDetectionCallsCorrectServiceCall() { // ARRANGE - var entityId = "camera.camera1"; - var service_call = "disable_motion_detection"; + const string? entityId = "camera.camera1"; + const string? service_call = "disable_motion_detection"; // ACT await DefaultDaemonApp .Cameras(new string[] { entityId }) .DisableMotionDetection() - .ExecuteAsync(); + .ExecuteAsync() + .ConfigureAwait(false); // ASSERT DefaultHassClientMock.VerifyCallServiceTimes(service_call, Times.Once()); @@ -61,8 +64,8 @@ await DefaultDaemonApp public async Task CamerasFuncDisableMotionDetectionCallsCorrectServiceCall() { // ARRANGE - var entityId = "camera.camera1"; - var service_call = "disable_motion_detection"; + const string? entityId = "camera.camera1"; + const string? service_call = "disable_motion_detection"; DefaultDaemonHost.InternalState["camera.camera1"] = new EntityState { @@ -74,7 +77,8 @@ public async Task CamerasFuncDisableMotionDetectionCallsCorrectServiceCall() await DefaultDaemonApp .Cameras(n => n.EntityId == entityId) .DisableMotionDetection() - .ExecuteAsync(); + .ExecuteAsync() + .ConfigureAwait(false); // ASSERT DefaultHassClientMock.VerifyCallServiceTimes(service_call, Times.Once()); @@ -85,14 +89,14 @@ await DefaultDaemonApp public async Task CameraEnableMotionDetectionCallsCorrectServiceCall() { // ARRANGE - var entityId = "camera.camera1"; - var service_call = "enable_motion_detection"; + const string? entityId = "camera.camera1"; + const string? service_call = "enable_motion_detection"; // ACT await DefaultDaemonApp .Camera(entityId) .EnableMotionDetection() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT DefaultHassClientMock.VerifyCallServiceTimes(service_call, Times.Once()); @@ -103,14 +107,14 @@ await DefaultDaemonApp public async Task CameraPlayStreamCallsCorrectServiceCall() { // ARRANGE - var entityId = "camera.camera1"; - var service_call = "play_stream"; + const string? entityId = "camera.camera1"; + const string? service_call = "play_stream"; // ACT await DefaultDaemonApp .Camera(entityId) .PlayStream("media_player.player", "anyformat") - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes(service_call, Times.Once()); @@ -125,14 +129,14 @@ await DefaultDaemonApp public async Task CameraRecordCallsCorrectServiceCall() { // ARRANGE - var entityId = "camera.camera1"; - var service_call = "record"; + const string? entityId = "camera.camera1"; + const string? service_call = "record"; // ACT await DefaultDaemonApp .Camera(entityId) .Record("filename", 1, 2) - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes(service_call, Times.Once()); @@ -148,14 +152,14 @@ await DefaultDaemonApp public async Task CameraSnapshotCallsCorrectServiceCall() { // ARRANGE - var entityId = "camera.camera1"; - var service_call = "snapshot"; + const string? entityId = "camera.camera1"; + const string? service_call = "snapshot"; // ACT await DefaultDaemonApp .Camera(entityId) .Snapshot("filename") - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes(service_call, Times.Once()); @@ -169,14 +173,14 @@ await DefaultDaemonApp public async Task CameraTurnOnCallsCorrectServiceCall() { // ARRANGE - var entityId = "camera.camera1"; - var service_call = "turn_on"; + const string? entityId = "camera.camera1"; + const string? service_call = "turn_on"; // ACT await DefaultDaemonApp .Camera(entityId) .TurnOn() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT DefaultHassClientMock.VerifyCallServiceTimes(service_call, Times.Once()); @@ -187,14 +191,14 @@ await DefaultDaemonApp public async Task CameraTurnOffCallsCorrectServiceCall() { // ARRANGE - var entityId = "camera.camera1"; - var service_call = "turn_off"; + const string? entityId = "camera.camera1"; + const string? service_call = "turn_off"; // ACT await DefaultDaemonApp .Camera(entityId) .TurnOff() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT DefaultHassClientMock.VerifyCallServiceTimes(service_call, Times.Once()); @@ -202,16 +206,17 @@ await DefaultDaemonApp } [Fact] - public async Task CamerassFuncExceptionLogsError() + [SuppressMessage("", "CA2201")] + public async Task CamerasFuncExceptionLogsError() { // ARRANGE DefaultDaemonHost.InternalState["id"] = new EntityState { EntityId = "id" }; // ACT var x = await Assert.ThrowsAsync(() => DefaultDaemonApp - .Cameras(n => throw new Exception("Some error")) + .Cameras(_ => throw new Exception("Some error")) .TurnOn() - .ExecuteAsync()); + .ExecuteAsync()).ConfigureAwait(false); // ASSERT DefaultHassClientMock.VerifyCallServiceTimes("turn_on", Times.Never()); diff --git a/tests/NetDaemon.Daemon.Tests/Fluent/FluentEventTests.cs b/tests/NetDaemon.Daemon.Tests/Fluent/FluentEventTests.cs index 7d18d1879..ebbc5e39f 100644 --- a/tests/NetDaemon.Daemon.Tests/Fluent/FluentEventTests.cs +++ b/tests/NetDaemon.Daemon.Tests/Fluent/FluentEventTests.cs @@ -2,6 +2,7 @@ using System.Dynamic; using System.Threading.Tasks; using Moq; +using NetDaemon.Common.Exceptions; using NetDaemon.Daemon.Fakes; using NetDaemon.Daemon.Storage; using Xunit; @@ -11,21 +12,23 @@ namespace NetDaemon.Daemon.Tests.Fluent public class FluentEventTests { [Fact] - public async Task ACustomEventNullValueCallThrowsNullReferenceException() + public async Task ACustomEventNullValueCallThrowsNetDaemonNullReferenceException() { // ARRANGE var hcMock = HassClientMock.DefaultMock; await using var daemonHost = new NetDaemonHost(hcMock.Object, new Mock().Object); - var app = new FluentTestApp(); - app.Id = "id"; + var app = new FluentTestApp + { + Id = "id" + }; daemonHost.InternalRunningAppInstances[app.Id] = app; - await app.StartUpAsync(daemonHost); + await app.StartUpAsync(daemonHost).ConfigureAwait(false); - var cancelSource = hcMock.GetSourceWithTimeout(); + var cancelSource = HassClientMock.GetSourceWithTimeout(); - Assert.Throws(() => app + Assert.Throws(() => app .Event("CUSTOM_EVENT") .Call(null).Execute()); } @@ -37,24 +40,26 @@ public async Task ACustomEventShouldDoCorrectCall() var hcMock = HassClientMock.DefaultMock; await using var daemonHost = new NetDaemonHost(hcMock.Object, new Mock().Object); - var app = new FluentTestApp(); - app.Id = "id"; + var app = new FluentTestApp + { + Id = "id" + }; daemonHost.InternalRunningAppInstances[app.Id] = app; - await app.StartUpAsync(daemonHost); + await app.StartUpAsync(daemonHost).ConfigureAwait(false); dynamic dynObject = new ExpandoObject(); dynObject.Test = "Hello World!"; hcMock.AddCustomEvent("CUSTOM_EVENT", dynObject); - var cancelSource = hcMock.GetSourceWithTimeout(); + var cancelSource = HassClientMock.GetSourceWithTimeout(); var isCalled = false; string? message = ""; app .Event("CUSTOM_EVENT") - .Call((ev, data) => + .Call((_, data) => { isCalled = true; message = data?.Test; @@ -67,7 +72,7 @@ public async Task ACustomEventShouldDoCorrectCall() } catch (TaskCanceledException) { - // Expected behaviour + // Expected behavior } Assert.True(isCalled); @@ -79,24 +84,26 @@ public async Task ACustomEventShouldUsingSelectorFuncDoCorrectCall() // ARRANGE var hcMock = HassClientMock.DefaultMock; await using var daemonHost = new NetDaemonHost(hcMock.Object, new Mock().Object); - var app = new FluentTestApp(); - app.Id = "id"; + var app = new FluentTestApp + { + Id = "id" + }; daemonHost.InternalRunningAppInstances[app.Id] = app; - await app.StartUpAsync(daemonHost); + await app.StartUpAsync(daemonHost).ConfigureAwait(false); dynamic dynObject = new ExpandoObject(); dynObject.Test = "Hello World!"; hcMock.AddCustomEvent("CUSTOM_EVENT", dynObject); - var cancelSource = hcMock.GetSourceWithTimeout(); + var cancelSource = HassClientMock.GetSourceWithTimeout(); var isCalled = false; string? message = ""; app .Events(n => n.EventId == "CUSTOM_EVENT") - .Call((ev, data) => + .Call((_, data) => { isCalled = true; message = data?.Test; @@ -109,7 +116,7 @@ public async Task ACustomEventShouldUsingSelectorFuncDoCorrectCall() } catch (TaskCanceledException) { - // Expected behaviour + // Expected behavior } Assert.True(isCalled); @@ -122,24 +129,26 @@ public async Task ACustomEventShouldUsingSelectorUsingDataFuncDoCorrectCall() // ARRANGE var hcMock = HassClientMock.DefaultMock; await using var daemonHost = new NetDaemonHost(hcMock.Object, new Mock().Object); - var app = new FluentTestApp(); - app.Id = "id"; + var app = new FluentTestApp + { + Id = "id" + }; daemonHost.InternalRunningAppInstances[app.Id] = app; - await app.StartUpAsync(daemonHost); + await app.StartUpAsync(daemonHost).ConfigureAwait(false); dynamic dynObject = new ExpandoObject(); dynObject.Test = "Hello World!"; hcMock.AddCustomEvent("CUSTOM_EVENT", dynObject); - var cancelSource = hcMock.GetSourceWithTimeout(); + var cancelSource = HassClientMock.GetSourceWithTimeout(); var isCalled = false; string? message = ""; app .Events(n => n.EventId == "CUSTOM_EVENT" && n?.Data?.Test == "Hello World!") - .Call((ev, data) => + .Call((_, data) => { isCalled = true; message = data?.Test; @@ -152,7 +161,7 @@ public async Task ACustomEventShouldUsingSelectorUsingDataFuncDoCorrectCall() } catch (TaskCanceledException) { - // Expected behaviour + // Expected behavior } Assert.True(isCalled); @@ -165,24 +174,26 @@ public async Task ACustomEventShouldUsingSelectorUsingDataFuncNotCall() // ARRANGE var hcMock = HassClientMock.DefaultMock; await using var daemonHost = new NetDaemonHost(hcMock.Object, new Mock().Object); - var app = new FluentTestApp(); - app.Id = "id"; + var app = new FluentTestApp + { + Id = "id" + }; daemonHost.InternalRunningAppInstances[app.Id] = app; - await app.StartUpAsync(daemonHost); + await app.StartUpAsync(daemonHost).ConfigureAwait(false); dynamic dynObject = new ExpandoObject(); dynObject.Test = "Hello World!"; hcMock.AddCustomEvent("CUSTOM_EVENT", dynObject); - var cancelSource = hcMock.GetSourceWithTimeout(); + var cancelSource = HassClientMock.GetSourceWithTimeout(); var isCalled = false; string? message = ""; app .Events(n => n.EventId == "CUSTOM_EVENT" && n?.Data?.Test == "Hello Test!") - .Call((ev, data) => + .Call((_, data) => { isCalled = true; message = data?.Test; @@ -195,7 +206,7 @@ public async Task ACustomEventShouldUsingSelectorUsingDataFuncNotCall() } catch (TaskCanceledException) { - // Expected behaviour + // Expected behavior } Assert.False(isCalled); @@ -207,23 +218,25 @@ public async Task ACustomEventShouldUsingSelectorUsingDataNotExisstFuncNotCall() // ARRANGE var hcMock = HassClientMock.DefaultMock; await using var daemonHost = new NetDaemonHost(hcMock.Object, new Mock().Object); - var app = new FluentTestApp(); - app.Id = "id"; + var app = new FluentTestApp + { + Id = "id" + }; daemonHost.InternalRunningAppInstances[app.Id] = app; - await app.StartUpAsync(daemonHost); + await app.StartUpAsync(daemonHost).ConfigureAwait(false); dynamic dynObject = new ExpandoObject(); dynObject.Test = "Hello World!"; hcMock.AddCustomEvent("CUSTOM_EVENT", dynObject); - var cancelSource = hcMock.GetSourceWithTimeout(); + var cancelSource = HassClientMock.GetSourceWithTimeout(); var isCalled = false; string? message = ""; app .Events(n => n.EventId == "CUSTOM_EVENT" && n?.Data?.NotExist == "Hello Test!") - .Call((ev, data) => + .Call((_, data) => { isCalled = true; message = data?.Test; @@ -236,7 +249,7 @@ public async Task ACustomEventShouldUsingSelectorUsingDataNotExisstFuncNotCall() } catch (TaskCanceledException) { - // Expected behaviour + // Expected behavior } Assert.False(isCalled); @@ -248,24 +261,26 @@ public async Task ACustomEventsShouldDoCorrectCall() // ARRANGE var hcMock = HassClientMock.DefaultMock; await using var daemonHost = new NetDaemonHost(hcMock.Object, new Mock().Object); - var app = new FluentTestApp(); - app.Id = "id"; + var app = new FluentTestApp + { + Id = "id" + }; daemonHost.InternalRunningAppInstances[app.Id] = app; - await app.StartUpAsync(daemonHost); + await app.StartUpAsync(daemonHost).ConfigureAwait(false); dynamic dynObject = new ExpandoObject(); dynObject.Test = "Hello World!"; hcMock.AddCustomEvent("CUSTOM_EVENT", dynObject); - var cancelSource = hcMock.GetSourceWithTimeout(); + var cancelSource = HassClientMock.GetSourceWithTimeout(); var isCalled = false; string? message = ""; app .Events(new string[] { "CUSTOM_EVENT" }) - .Call((ev, data) => + .Call((_, data) => { isCalled = true; message = data?.Test; @@ -278,7 +293,7 @@ public async Task ACustomEventsShouldDoCorrectCall() } catch (TaskCanceledException) { - // Expected behaviour + // Expected behavior } Assert.True(isCalled); diff --git a/tests/NetDaemon.Daemon.Tests/Fluent/FluentTests.cs b/tests/NetDaemon.Daemon.Tests/Fluent/FluentTests.cs index 407407b84..ef6c6e4cc 100644 --- a/tests/NetDaemon.Daemon.Tests/Fluent/FluentTests.cs +++ b/tests/NetDaemon.Daemon.Tests/Fluent/FluentTests.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Dynamic; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -46,9 +48,9 @@ public async Task EntityOnStateChangedForTimeTurnOffLightCallsCorrectServiceCall lastUpdated, lastChanged); // ASSERT - await Task.Delay(10); // After 10ms we should not have call + await Task.Delay(10).ConfigureAwait(false); // After 10ms we should not have call VerifyCallServiceTimes("turn_off", Times.Never()); - await Task.Delay(300); // After 30ms we should have call + await Task.Delay(300).ConfigureAwait(false); // After 30ms we should have call VerifyCallServiceTimes("turn_off", Times.Once()); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); } @@ -79,7 +81,7 @@ public async Task EntityOnStateChangedLamdaWithMultipleEntitiesCallsCorrectServi // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var MotionEnabled = true; + const bool MotionEnabled = true; DefaultDaemonApp .Entities(new string[] { "binary_sensor.pir", "binary_sensor-pir2" }) @@ -128,7 +130,6 @@ public async Task EntityOnStateChangedMultipleTimesCallsCorrectServiceCall() .TurnOn() .Execute(); - AddChangedEvent("binary_sensor.pir", "off", "on"); AddChangedEvent("binary_sensor.pir", "off", "on"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); @@ -184,8 +185,6 @@ public async Task EntityOnStateChangedEntitiesLambdaTurnOnLightCallsCorrectServi // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - - // Fake the DefaultDaemonHost.InternalState["light.correct_entity"] = new EntityState { @@ -212,7 +211,6 @@ public async Task EntityOnStateChangedTurnOnLightCallsCorrectServiceCallButNoTur // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - DefaultDaemonApp .Entity("binary_sensor.pir") .WhenStateChange("on") @@ -272,7 +270,7 @@ public async Task EntityOnStateDefaultTriggerOnAnyStateChange() DefaultDaemonApp .Entity("binary_sensor.pir") .WhenStateChange() - .Call((e, n, o) => + .Call((_, _, _) => { triggered = true; return Task.CompletedTask; @@ -298,7 +296,7 @@ public async Task EntityOnStateNotTriggerOnSameState() DefaultDaemonApp .Entity("binary_sensor.pir") .WhenStateChange() - .Call((e, n, o) => + .Call((_, _, _) => { triggered = true; return Task.CompletedTask; @@ -325,7 +323,7 @@ public async Task EntityOnStateIncludeAttributesTriggerOnSameState() DefaultDaemonApp .Entity("binary_sensor.pir") .WhenStateChange(allChanges: true) - .Call((e, n, o) => + .Call((_, _, _) => { triggered = true; return Task.CompletedTask; @@ -347,7 +345,8 @@ public async Task ToggleEntityCallsCorrectServiceCall() await DefaultDaemonApp .Entity("light.correct_entity") .Toggle() - .ExecuteAsync(); + .ExecuteAsync() + .ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("toggle", Times.Once()); @@ -363,7 +362,8 @@ public async Task TurnOffEntityCallsCorrectServiceCall() await DefaultDaemonApp .Entity("light.correct_entity") .TurnOff() - .ExecuteAsync(); + .ExecuteAsync() + .ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("turn_off", Times.Once()); @@ -371,6 +371,7 @@ await DefaultDaemonApp } [Fact] + [SuppressMessage("", "CA2201")] public async Task EntityFuncExceptionLogsError() { // ARRANGE @@ -378,9 +379,9 @@ public async Task EntityFuncExceptionLogsError() // ACT var x = await Assert.ThrowsAsync(() => DefaultDaemonApp - .Entities(n => throw new Exception("Some error")) + .Entities(_ => throw new Exception("Some error")) .TurnOff() - .ExecuteAsync()); + .ExecuteAsync()).ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("turn_off", Times.Never()); @@ -398,7 +399,7 @@ public async Task TurnOffEntityLambdaAttributeSelectionCallsCorrectServiceCall() await DefaultDaemonApp .Entities(n => n?.Attribute?.test >= 100) .TurnOff() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT @@ -418,7 +419,7 @@ public async Task TurnOffEntityLambdaAttributeSelectionNoExistCallsCorrectServic await DefaultDaemonApp .Entities(n => n?.Attribute?.not_exists == "test") .TurnOff() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT @@ -432,9 +433,9 @@ public async Task TurnOffEntityLamdaSelectionCallsCorrectServiceCall() await InitializeFakeDaemon().ConfigureAwait(false); // ACT await DefaultDaemonApp - .Entities(n => n.EntityId.StartsWith("light.correct_entity")) + .Entities(n => n.EntityId.StartsWith("light.correct_entity", true, CultureInfo.InvariantCulture)) .TurnOff() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT @@ -454,7 +455,7 @@ public async Task TurnOffMultipleEntityCallsCorrectServiceCall() await DefaultDaemonApp .Entity("light.correct_entity", "light.correct_entity2") .TurnOff() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("turn_off", Times.Exactly(2)); @@ -471,7 +472,7 @@ public async Task TurnOnEntityCallsCorrectServiceCall() await DefaultDaemonApp .Entity("light.correct_entity") .TurnOn() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("turn_on", Times.Once()); @@ -487,7 +488,7 @@ await DefaultDaemonApp .Entity("light.correct_entity") .TurnOn() .WithAttribute("brightness", 100) - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("turn_on", Times.Once()); @@ -506,7 +507,7 @@ await DefaultDaemonApp .TurnOn() .WithAttribute("brightness", 100) .WithAttribute("color_temp", 230) - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("turn_on", Times.Once()); @@ -524,7 +525,7 @@ public async Task SetStateEntityCallsCorrectServiceCall() await DefaultDaemonApp .Entity("light.correct_entity") .SetState(50) - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifySetStateTimes("light.correct_entity", Times.Once()); @@ -540,7 +541,7 @@ await DefaultDaemonApp .Entity("light.correct_entity") .SetState(50) .WithAttribute("attr1", "str_value") - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifySetStateTimes("light.correct_entity", Times.Once()); @@ -605,14 +606,14 @@ public async Task SpeakCallsCorrectServiceCall() await DefaultDaemonApp .MediaPlayer("media_player.correct_player") .Speak("a message") - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); - await Task.Delay(20); + await Task.Delay(20).ConfigureAwait(false); var expObject = new FluentExpandoObject(); - dynamic expectedAttruibutes = expObject; - expectedAttruibutes.entity_id = "media_player.correct_player"; - expectedAttruibutes.message = "a message"; + dynamic expectedAttributes = expObject; + expectedAttributes.entity_id = "media_player.correct_player"; + expectedAttributes.message = "a message"; // ASSERT VerifyCallService("tts", "google_cloud_say", expObject, true); @@ -629,7 +630,7 @@ public async Task MediaPlayerPlayCallsCorrectServiceCall() await DefaultDaemonApp .MediaPlayer("media_player.player") .Play() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("media_play", Times.Once()); @@ -650,7 +651,7 @@ public async Task MediaPlayersFuncPlayCallsCorrectServiceCall() await DefaultDaemonApp .MediaPlayers(n => n.EntityId == "media_player.player") .Play() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("media_play", Times.Once()); @@ -658,6 +659,7 @@ await DefaultDaemonApp } [Fact] + [SuppressMessage("", "CA2201")] public async Task MediaPlayersFuncExceptionLogsError() { // ARRANGE @@ -665,9 +667,9 @@ public async Task MediaPlayersFuncExceptionLogsError() // ACT var x = await Assert.ThrowsAsync(() => DefaultDaemonApp - .MediaPlayers(n => throw new Exception("Some error")) + .MediaPlayers(_ => throw new Exception("Some error")) .Play() - .ExecuteAsync()); + .ExecuteAsync()).ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("turn_off", Times.Never()); @@ -683,7 +685,7 @@ public async Task MediaPlayersPlayCallsCorrectServiceCall() await DefaultDaemonApp .MediaPlayers(new string[] { "media_player.player" }) .Play() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("media_play", Times.Once()); @@ -698,7 +700,7 @@ public async Task MediaPlayerPauseCallsCorrectServiceCall() await DefaultDaemonApp .MediaPlayer("media_player.player") .Pause() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("media_pause", Times.Once()); @@ -713,7 +715,7 @@ public async Task MediaPlayerPlayPauseCallsCorrectServiceCall() await DefaultDaemonApp .MediaPlayer("media_player.player") .PlayPause() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("media_play_pause", Times.Once()); @@ -728,7 +730,7 @@ public async Task MediaPlayerStopCallsCorrectServiceCall() await DefaultDaemonApp .MediaPlayer("media_player.player") .Stop() - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("media_stop", Times.Once()); @@ -743,7 +745,7 @@ public async Task InputSelectSetOptionShouldCallCorrectCallService() await DefaultDaemonApp .InputSelect("input_select.myselect") .SetOption("option1") - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("select_option", Times.Once()); @@ -760,7 +762,7 @@ public async Task InputSelectSetOptionIEnumerableShouldCallCorrectCallService() await DefaultDaemonApp .InputSelects(new string[] { "input_select.myselect" }) .SetOption("option1") - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("select_option", Times.Once()); @@ -778,7 +780,7 @@ public async Task InputSelectSetOptionFuncShouldCallCorrectCallService() await DefaultDaemonApp .InputSelects(n => n.EntityId == "input_select.myselect") .SetOption("option1") - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("select_option", Times.Once()); @@ -810,7 +812,6 @@ public async Task EntityDelayUntilStateChangeLamdaShouldReturnTrue() // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var delayResult = DefaultDaemonApp .Entity("binary_sensor.pir") .DelayUntilStateChange((to, _) => to?.State == "on"); @@ -829,7 +830,7 @@ public async Task EntityInAreaOnStateChangedTurnOnLight() await InitializeFakeDaemon().ConfigureAwait(false); // Fake the state - SetEntityState("light.ligth_in_area", area: "Area"); + SetEntityState("light.light_in_area", area: "Area"); // ACT DefaultDaemonApp @@ -840,7 +841,7 @@ public async Task EntityInAreaOnStateChangedTurnOnLight() .Execute(); // light.light_in_area is setup so it has area = Area - AddChangedEvent("light.ligth_in_area", "off", "on"); + AddChangedEvent("light.light_in_area", "off", "on"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); @@ -857,7 +858,7 @@ public async Task EntityInAreaOnStateChangedShouldTurnOn() await InitializeFakeDaemon().ConfigureAwait(false); // Fake the state - SetEntityState("light.ligth_in_area", area: "Area"); + SetEntityState("light.light_in_area", area: "Area"); // ACT DefaultDaemonApp @@ -873,7 +874,7 @@ public async Task EntityInAreaOnStateChangedShouldTurnOn() await RunFakeDaemonUntilTimeout().ConfigureAwait(false); // ASSERT VerifyCallServiceTimes("turn_on", Times.Once()); - VerifyCallServiceTuple("light", "turn_on", ("entity_id", "light.ligth_in_area")); + VerifyCallServiceTuple("light", "turn_on", ("entity_id", "light.light_in_area")); } [Fact] @@ -883,8 +884,8 @@ public async Task RunScriptShouldCallCorrectService() await InitializeFakeDaemon().ConfigureAwait(false); // ACT - await DefaultDaemonApp.RunScript("testscript").ExecuteAsync(); - await DefaultDaemonApp.RunScript("script.testscript").ExecuteAsync(); + await DefaultDaemonApp.RunScript("testscript").ExecuteAsync().ConfigureAwait(false); + await DefaultDaemonApp.RunScript("script.testscript").ExecuteAsync().ConfigureAwait(false); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); // ASSERT @@ -898,12 +899,11 @@ public async Task SendEventShouldCallCorrectService() await InitializeFakeDaemon().ConfigureAwait(false); // ACT - await DefaultDaemonApp.SendEvent("myevent"); + await DefaultDaemonApp.SendEvent("myevent").ConfigureAwait(false); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); // ASSERT VerifyEventSent("myevent"); } } - } \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/NetDaemon.Daemon.Tests.csproj b/tests/NetDaemon.Daemon.Tests/NetDaemon.Daemon.Tests.csproj index 3c3e6a919..fa9960f8d 100644 --- a/tests/NetDaemon.Daemon.Tests/NetDaemon.Daemon.Tests.csproj +++ b/tests/NetDaemon.Daemon.Tests/NetDaemon.Daemon.Tests.csproj @@ -25,6 +25,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + @@ -59,7 +63,10 @@ Always - - + + ..\..\.linting\roslynator.ruleset + true + AllEnabledByDefault + diff --git a/tests/NetDaemon.Daemon.Tests/NetDaemonApp/AppExtensionsTests.cs b/tests/NetDaemon.Daemon.Tests/NetDaemonApp/AppExtensionsTests.cs index 57a4b1475..c504382f3 100644 --- a/tests/NetDaemon.Daemon.Tests/NetDaemonApp/AppExtensionsTests.cs +++ b/tests/NetDaemon.Daemon.Tests/NetDaemonApp/AppExtensionsTests.cs @@ -16,7 +16,7 @@ public void TestToSafeHomeAssistantEntityIdReturnCorrectString(string convert, s //ACT string converted = convert.ToSafeHomeAssistantEntityId(); - Assert.Equal(expected.Length, converted.Length); + Assert.Equal(expected?.Length, converted.Length); Assert.Equal(expected, converted); } } diff --git a/tests/NetDaemon.Daemon.Tests/NetDaemonApp/FaultyAppsTests.cs b/tests/NetDaemon.Daemon.Tests/NetDaemonApp/FaultyAppsTests.cs index 40cf46ce1..004f4c4a9 100644 --- a/tests/NetDaemon.Daemon.Tests/NetDaemonApp/FaultyAppsTests.cs +++ b/tests/NetDaemon.Daemon.Tests/NetDaemonApp/FaultyAppsTests.cs @@ -1,29 +1,27 @@ -using JoySoftware.HomeAssistant.Client; using Microsoft.Extensions.Logging; using Moq; using NetDaemon.Daemon.Fakes; -using System; -using System.Collections.Generic; +using System.Globalization; using System.Threading.Tasks; using Xunit; namespace NetDaemon.Daemon.Tests.NetDaemonApp { - public class DaemonAppTestApp : NetDaemon.Common.NetDaemonApp { } + public class DaemonAppTestApp : Common.NetDaemonApp { } public class FaultyAppTests : DaemonHostTestBase { - private readonly NetDaemon.Common.NetDaemonApp _app; - - public FaultyAppTests() : base() + public FaultyAppTests() { - _app = new DaemonAppTestApp(); - _app.Id = "id"; - DefaultDaemonHost.InternalRunningAppInstances[_app.Id] = App; - _app.StartUpAsync(DefaultDaemonHost).Wait(); + App = new DaemonAppTestApp + { + Id = "id" + }; + DefaultDaemonHost.InternalRunningAppInstances[App.Id] = App; + App.StartUpAsync(DefaultDaemonHost).Wait(); } - public NetDaemon.Common.NetDaemonApp App => _app; + public Common.NetDaemonApp App { get; } [Fact] public async Task ARunTimeErrorShouldLogError() @@ -34,10 +32,10 @@ public async Task ARunTimeErrorShouldLogError() App .Entity("binary_sensor.pir") .WhenStateChange("on") - .Call((entity, from, to) => + .Call((_, _, _) => { // Do conversion error - int x = int.Parse("ss"); + int x = int.Parse("ss", CultureInfo.InvariantCulture); return Task.CompletedTask; }).Execute(); @@ -57,17 +55,17 @@ public async Task ARunTimeErrorShouldNotBreakOtherApps() App .Entity("binary_sensor.pir") .WhenStateChange("on") - .Call((entity, from, to) => + .Call((_, _, _) => { // Do conversion error - int x = int.Parse("ss"); + int x = int.Parse("ss", CultureInfo.InvariantCulture); return Task.CompletedTask; }).Execute(); App .Entity("binary_sensor.pir") .WhenStateChange("on") - .Call((entity, from, to) => + .Call((_, _, _) => { // Do conversion error eventRun = true; @@ -80,7 +78,6 @@ public async Task ARunTimeErrorShouldNotBreakOtherApps() LoggerMock.AssertLogged(LogLevel.Error, Times.Once()); Assert.True(eventRun); - } [Fact] @@ -92,17 +89,17 @@ public async Task MissingAttributeShouldNotBreakOtherApps() App .Entities(e => e.Attribute!.does_not_exist == "yay") .WhenStateChange() - .Call((entity, from, to) => + .Call((_, _, _) => { // Do conversion error - int x = int.Parse("ss"); + int x = int.Parse("ss", CultureInfo.InvariantCulture); return Task.CompletedTask; }).Execute(); App .Entity("binary_sensor.pir") .WhenStateChange("on") - .Call((entity, from, to) => + .Call((_, _, _) => { // Do conversion error eventRun = true; @@ -113,9 +110,7 @@ public async Task MissingAttributeShouldNotBreakOtherApps() await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // LoggerMock.AssertLogged(LogLevel.Error, Times.Once()); Assert.True(eventRun); - } [Fact] @@ -127,7 +122,7 @@ public async Task MissingEntityShouldNotBreakOtherApps() App .Entity("binary_sensor.pir") .WhenStateChange() - .Call((entity, from, to) => + .Call((_, _, _) => { // Do conversion error App.Entity("does_not_exist").TurnOn(); @@ -137,7 +132,7 @@ public async Task MissingEntityShouldNotBreakOtherApps() App .Entity("binary_sensor.pir") .WhenStateChange("on") - .Call((entity, from, to) => + .Call((_, _, _) => { // Do conversion error eventRun = true; @@ -149,37 +144,6 @@ public async Task MissingEntityShouldNotBreakOtherApps() await RunFakeDaemonUntilTimeout().ConfigureAwait(false); Assert.True(eventRun); - - } - - private void AddDefaultEvent() - { - DefaultHassClientMock.FakeEvents.Enqueue(new HassEvent - { - EventType = "state_changed", - Data = new HassStateChangedEventData - { - EntityId = "binary_sensor.pir", - NewState = new HassState - { - State = "on", - Attributes = new Dictionary - { - ["device_class"] = "motion" - }, - LastChanged = DateTime.Now, - LastUpdated = DateTime.Now - }, - OldState = new HassState - { - State = "off", - Attributes = new Dictionary - { - ["device_class"] = "motion" - } - } - } - }); } } } \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/NetDaemonApp/FaultyRxAppsTests.cs b/tests/NetDaemon.Daemon.Tests/NetDaemonApp/FaultyRxAppsTests.cs index 83dfa470e..efd73beb4 100644 --- a/tests/NetDaemon.Daemon.Tests/NetDaemonApp/FaultyRxAppsTests.cs +++ b/tests/NetDaemon.Daemon.Tests/NetDaemonApp/FaultyRxAppsTests.cs @@ -7,6 +7,7 @@ using Xunit; using System.Reactive.Linq; using NetDaemon.Daemon.Fakes; +using System.Globalization; namespace NetDaemon.Daemon.Tests.NetDaemonApp { @@ -14,17 +15,17 @@ public class DaemonRxAppTestApp : NetDaemon.Common.Reactive.NetDaemonRxApp { } public class FaultyRxAppTests : DaemonHostTestBase { - private readonly NetDaemon.Common.Reactive.NetDaemonRxApp _app; - public FaultyRxAppTests() : base() { - _app = new DaemonRxAppTestApp(); - _app.Id = "id"; - DefaultDaemonHost.InternalRunningAppInstances[_app.Id] = App; - _app.StartUpAsync(DefaultDaemonHost).Wait(); + App = new DaemonRxAppTestApp + { + Id = "id" + }; + DefaultDaemonHost.InternalRunningAppInstances[App.Id] = App; + App.StartUpAsync(DefaultDaemonHost).Wait(); } - public NetDaemon.Common.Reactive.NetDaemonRxApp App => _app; + public NetDaemon.Common.Reactive.NetDaemonRxApp App { get; } [Fact] public async Task ARunTimeErrorShouldLogError() @@ -35,9 +36,9 @@ public async Task ARunTimeErrorShouldLogError() App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => + .Subscribe(_ => { - int x = int.Parse("ss"); + int x = int.Parse("ss", CultureInfo.InvariantCulture); }); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); @@ -56,10 +57,7 @@ public async Task MissingEntityShouldNotLogError() App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - App.Entity("light.do_not_exist").TurnOn(); - }); + .Subscribe(_ => App.Entity("light.do_not_exist").TurnOn()); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); @@ -79,26 +77,17 @@ public async Task MissingEntityShouldNotLogErrorAndNotBreakOtherApps() App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - eventRun = true; - }); + .Subscribe(_ => eventRun = true); App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - App.Entity("light.do_not_exist").TurnOn(); - }); + .Subscribe(_ => App.Entity("light.do_not_exist").TurnOn()); App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - event2Run = true; - }); + .Subscribe(_ => event2Run = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); @@ -107,7 +96,6 @@ public async Task MissingEntityShouldNotLogErrorAndNotBreakOtherApps() LoggerMock.AssertLogged(LogLevel.Error, Times.Never()); Assert.True(eventRun); Assert.True(event2Run); - } [Fact] @@ -121,26 +109,20 @@ public async Task ARunTimeErrorShouldNotBreakOtherApps() App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - eventRun = true; - }); + .Subscribe(_ => eventRun = true); App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => + .Subscribe(_ => { - int x = int.Parse("ss"); + int x = int.Parse("ss", CultureInfo.InvariantCulture); }); App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - event2Run = true; - }); + .Subscribe(_ => event2Run = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); @@ -149,7 +131,6 @@ public async Task ARunTimeErrorShouldNotBreakOtherApps() LoggerMock.AssertLogged(LogLevel.Error, Times.Once()); Assert.True(eventRun); Assert.True(event2Run); - } [Fact] @@ -163,27 +144,21 @@ public async Task ARunTimeErrorInAttributeSelectorShouldNotBreakOtherApps() App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - eventRun = true; - }); + .Subscribe(_ => eventRun = true); App .Entity("binary_sensor.pir") .StateChanges .Where(e => e.New.Attribute!.an_int == "WTF this is not an int!!") - .Subscribe(s => + .Subscribe(_ => { - int x = int.Parse("ss"); + int x = int.Parse("ss", CultureInfo.InvariantCulture); }); App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - event2Run = true; - }); + .Subscribe(_ => event2Run = true); AddDefaultEvent(); @@ -192,10 +167,8 @@ public async Task ARunTimeErrorInAttributeSelectorShouldNotBreakOtherApps() LoggerMock.AssertLogged(LogLevel.Error, Times.Once()); Assert.True(eventRun); Assert.True(event2Run); - } - [Fact] public async Task ToUnavailableShouldNotBreak() { @@ -207,27 +180,21 @@ public async Task ToUnavailableShouldNotBreak() App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - eventRun = true; - }); + .Subscribe(_ => eventRun = true); App .Entity("binary_sensor.pir") .StateChanges .Where(e => e.New.State == "on") - .Subscribe(s => + .Subscribe(_ => { - int x = int.Parse("ss"); + int x = int.Parse("ss", CultureInfo.InvariantCulture); }); App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - event2Run = true; - }); + .Subscribe(_ => event2Run = true); AddEventFakeGoingUnavailable(); @@ -236,7 +203,6 @@ public async Task ToUnavailableShouldNotBreak() LoggerMock.AssertLogged(LogLevel.Error, Times.Never()); Assert.False(eventRun); Assert.False(event2Run); - } [Fact] @@ -250,27 +216,21 @@ public async Task FromUnavailableShouldNotBreak() App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - eventRun = true; - }); + .Subscribe(_ => eventRun = true); App .Entity("binary_sensor.pir") .StateChanges .Where(e => e.New.State == "on") - .Subscribe(s => + .Subscribe(_ => { - int x = int.Parse("ss"); + int x = int.Parse("ss", CultureInfo.InvariantCulture); }); App .Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - event2Run = true; - }); + .Subscribe(_ => event2Run = true); AddEventFakeFromUnavailable(); @@ -279,76 +239,8 @@ public async Task FromUnavailableShouldNotBreak() LoggerMock.AssertLogged(LogLevel.Error, Times.Never()); Assert.False(eventRun); Assert.False(event2Run); - } - // [Fact] - // public async Task MissingAttributeShouldNotBreakOtherApps() - // { - // // ARRANGE - // bool eventRun = false; - // App - // .Entities(e => e.Attribute.does_not_exist == "yay") - // .WhenStateChange() - // .Call((entity, from, to) => - // { - // // Do conversion error - // int x = int.Parse("ss"); - // return Task.CompletedTask; - // }).Execute(); - - // App - // .Entity("binary_sensor.pir") - // .WhenStateChange("on") - // .Call((entity, from, to) => - // { - // // Do conversion error - // eventRun = true; - // return Task.CompletedTask; - // }).Execute(); - - // DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); - - // await RunDefauldDaemonUntilCanceled(); - - // // LoggerMock.AssertLogged(LogLevel.Error, Times.Once()); - // Assert.True(eventRun); - - // } - - // [Fact] - // public async Task MissingEntityShouldNotBreakOtherApps() - // { - // // ARRANGE - // bool eventRun = false; - // App - // .Entity("binary_sensor.pir") - // .WhenStateChange() - // .Call((entity, from, to) => - // { - // // Do conversion error - // App.Entity("does_not_exist").TurnOn(); - // return Task.CompletedTask; - // }).Execute(); - - // App - // .Entity("binary_sensor.pir") - // .WhenStateChange("on") - // .Call((entity, from, to) => - // { - // // Do conversion error - // eventRun = true; - // return Task.CompletedTask; - // }).Execute(); - - // DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); - - // await RunDefauldDaemonUntilCanceled(); - - // Assert.True(eventRun); - - // } - private void AddDefaultEvent() { DefaultHassClientMock.FakeEvents.Enqueue(new HassEvent diff --git a/tests/NetDaemon.Daemon.Tests/NetDaemonApp/NetDaemonAppTests.cs b/tests/NetDaemon.Daemon.Tests/NetDaemonApp/NetDaemonAppTests.cs index a25def45c..41edc8682 100644 --- a/tests/NetDaemon.Daemon.Tests/NetDaemonApp/NetDaemonAppTests.cs +++ b/tests/NetDaemon.Daemon.Tests/NetDaemonApp/NetDaemonAppTests.cs @@ -7,19 +7,21 @@ using NetDaemon.Common.Fluent; using Xunit; using NetDaemon.Daemon.Fakes; +using NetDaemon.Common.Exceptions; namespace NetDaemon.Daemon.Tests.NetDaemonApp { - public class AppTestApp : NetDaemon.Common.NetDaemonApp { } + public class AppTestApp : Common.NetDaemonApp { } - public class AppTestApp2 : NetDaemon.Common.NetDaemonApp { } + public class AppTestApp2 : Common.NetDaemonApp { } - public class NetDaemonApptests + public class NetDaemonApptests : IAsyncLifetime, IDisposable { private const string appTemplate = " app: "; private readonly LoggerMock _logMock; - private NetDaemon.Common.NetDaemonApp _app; - private Mock _netDaemonMock; + private readonly Common.NetDaemonApp _app; + private readonly Mock _netDaemonMock; + private bool disposedValue; public NetDaemonApptests() { @@ -27,13 +29,13 @@ public NetDaemonApptests() _netDaemonMock = new Mock(); _netDaemonMock.SetupGet(n => n.Logger).Returns(_logMock.Logger); - _appMock = new Mock(); + AppMock = new Mock(); _app = new AppTestApp(); _app.StartUpAsync(_netDaemonMock.Object); _app.Id = "app"; } - public Mock _appMock { get; } + public Mock AppMock { get; } [Fact] public void CallServiceShouldCallCorrectDaemonCallService() @@ -87,7 +89,7 @@ public void EnitiesFuncShouldCallCorrectDaemonEntity() } [Fact] - public void EnitiesShouldCallCorrectDaemonEntity() + public void EntitiesShouldCallCorrectDaemonEntity() { // ARRANGE and ACT _app.Entities(new string[] { "light.somelight" }); @@ -96,7 +98,7 @@ public void EnitiesShouldCallCorrectDaemonEntity() } [Fact] - public void EnityShouldCallCorrectDaemonEntity() + public void EntityShouldCallCorrectDaemonEntity() { // ARRANGE and ACT _app.Entity("light.somelight"); @@ -120,7 +122,7 @@ public void GetStateShouldCallCorrectDaemonGetState() public async Task GlobalShouldReturnCorrectData(string key, object value) { await using var _app_two = new AppTestApp2(); - await _app_two.StartUpAsync(_netDaemonMock.Object); + await _app_two.StartUpAsync(_netDaemonMock.Object).ConfigureAwait(false); _app_two.Id = "app2"; // ARRANGE and ACT @@ -139,11 +141,10 @@ public async Task GlobalShouldReturnCorrectData(string key, object value) public void LogMessageWithDifferentLogLevelsShoulCallCorrectLogger(LogLevel level, string methodName) { // ARRANGE - var message = "message"; - MethodInfo? methodInfo; + const string? message = "message"; // ACT - methodInfo = _app.GetType().GetMethod(methodName, new Type[] { typeof(string) }); + MethodInfo? methodInfo = _app.GetType().GetMethod(methodName, new Type[] { typeof(string) }); methodInfo?.Invoke(_app, new object[] { message }); // ASSERT @@ -159,8 +160,8 @@ public void LogMessageWithDifferentLogLevelsShoulCallCorrectLogger(LogLevel leve public void LogMessageWithExceptionAndDifferentLogLevelsShoulCallCorrectLogger(LogLevel level, string methodName) { // ARRANGE - var message = "message"; - var exception = new NullReferenceException("Null"); + const string? message = "message"; + var exception = new NetDaemonNullReferenceException("Null"); // ACT var methodInfo = _app.GetType().GetMethod(methodName, new Type[] { typeof(Exception), typeof(string) }); @@ -178,7 +179,7 @@ public void LogMessageWithExceptionAndDifferentLogLevelsShoulCallCorrectLogger(L public void LogMessageWithParamsAndDifferentLogLevelsShoulCallCorrectLogger(LogLevel level, string methodName) { // ARRANGE - var message = "Hello {name}"; + const string? message = "Hello {name}"; // ACT var methodInfo = _app.GetType().GetMethod(methodName, new Type[] { typeof(string), typeof(object[]) }); @@ -197,8 +198,8 @@ public void LogMessageWithParamsAndDifferentLogLevelsShoulCallCorrectLogger(LogL public void LogMessageWithParamsExceptionAndDifferentLogLevelsShoulCallCorrectLogger(LogLevel level, string methodName) { // ARRANGE - var message = "Hello {name}"; - var exception = new NullReferenceException("Null"); + const string? message = "Hello {name}"; + var exception = new NetDaemonNullReferenceException("Null"); // ACT var methodInfo = _app.GetType().GetMethod(methodName, new Type[] { typeof(Exception), typeof(string), typeof(object[]) }); @@ -206,5 +207,34 @@ public void LogMessageWithParamsExceptionAndDifferentLogLevelsShoulCallCorrectLo // ASSERT _logMock.AssertLogged(level, exception, appTemplate + "Hello Bob", Times.Once()); } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public Task InitializeAsync() + { + return Task.CompletedTask; + } + + public async Task DisposeAsync() + { + await _app.DisposeAsync().ConfigureAwait(false); + } } } \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/Reactive/RxAppTest.cs b/tests/NetDaemon.Daemon.Tests/Reactive/RxAppTest.cs index fbed47a59..4a9f99bca 100644 --- a/tests/NetDaemon.Daemon.Tests/Reactive/RxAppTest.cs +++ b/tests/NetDaemon.Daemon.Tests/Reactive/RxAppTest.cs @@ -1,9 +1,11 @@ using System; using System.Dynamic; +using System.Globalization; using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Moq; +using NetDaemon.Common.Exceptions; using NetDaemon.Common.Reactive; using Xunit; @@ -27,14 +29,13 @@ public async Task CallServiceShouldCallCorrectFunction() { // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var (dynObj, expObj) = GetDynamicObject( + var (dynObj, _) = GetDynamicObject( ("attr", "value")); // ACT DefaultDaemonRxApp.CallService("mydomain", "myservice", dynObj); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT VerifyCallServiceTuple("mydomain", "myservice", ("attr", "value")); } @@ -48,16 +49,12 @@ public async Task NewAllEventDataShouldCallFunction() // ACT DefaultDaemonRxApp.StateAllChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "on", "on"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.True(called); } @@ -71,16 +68,12 @@ public async Task NewEventShouldCallFunction() // ACT DefaultDaemonRxApp.EventChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddCustomEvent("AN_EVENT", new { somedata = "hello" }); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.True(called); } @@ -95,10 +88,7 @@ public async Task NewEventMissingDataAttributeShouldReturnNull() // ACT DefaultDaemonRxApp.EventChanges - .Subscribe(s => - { - missingAttribute = s.Data?.missing_data; - }); + .Subscribe(s => missingAttribute = s.Data?.missing_data); var expandoObj = new ExpandoObject(); dynamic dynExpObject = expandoObj; @@ -108,7 +98,6 @@ public async Task NewEventMissingDataAttributeShouldReturnNull() await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.Null(missingAttribute); } @@ -122,16 +111,12 @@ public async Task NewStateEventShouldCallFunction() // ACT DefaultDaemonRxApp.StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.True(called); } @@ -141,17 +126,13 @@ public async Task RunScriptShouldCallCorrectFunction() { // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var (dynObj, expObj) = GetDynamicObject( - ("attr", "value")); // ACT DefaultDaemonRxApp.RunScript("myscript"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT - DefaultHassClientMock.VerifyCallServiceTimes("myscript", Times.Once()); } @@ -160,14 +141,11 @@ public async Task RunScriptWithDomainShouldCallCorrectFunction() { // ARRANGE await InitializeFakeDaemon().ConfigureAwait(false); - var (dynObj, expObj) = GetDynamicObject( - ("attr", "value")); // ACT DefaultDaemonRxApp.RunScript("script.myscript"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT DefaultHassClientMock.VerifyCallServiceTimes("myscript", Times.Once()); } @@ -181,16 +159,12 @@ public async Task SameStateEventShouldNotCallFunction() // ACT DefaultDaemonRxApp.StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "on", "on"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.False(called); } @@ -206,7 +180,6 @@ public async Task SetStateShouldReturnCorrectData() DefaultDaemonRxApp.SetState("sensor.any_sensor", "on", dynObj); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT DefaultHassClientMock.Verify(n => n.SetState("sensor.any_sensor", "on", expObj)); } @@ -217,7 +190,7 @@ public async Task StartupAsyncShouldThrowIfDaemonIsNull() INetDaemonHost? host = null; // ARRANGE ACT ASSERT - await Assert.ThrowsAsync(() => DefaultDaemonRxApp.StartUpAsync(host!)); + await Assert.ThrowsAsync(() => DefaultDaemonRxApp.StartUpAsync(host!)).ConfigureAwait(false); } [Fact] @@ -231,7 +204,6 @@ public async Task StateShouldReturnCorrectEntity() await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.NotNull(entity); } @@ -247,7 +219,6 @@ public async Task StateShouldReturnNullIfAttributeNotExist() await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.Null(entity?.Attribute?.not_exists); } @@ -277,7 +248,7 @@ public async Task EntityIdsShouldReturnCorrectItems() await RunFakeDaemonUntilTimeout().ConfigureAwait(false); // ASSERT Assert.NotNull(entities); - Assert.Equal(8, entities.Count()); + Assert.Equal(8, entities.Count); } [Fact] @@ -288,18 +259,14 @@ public async Task UsingEntitiesLambdaNewEventShouldCallFunction() var called = false; // ACT - DefaultDaemonRxApp.Entities(n => n.EntityId.StartsWith("binary_sensor.pir")) + DefaultDaemonRxApp.Entities(n => n.EntityId.StartsWith("binary_sensor.pir", true, CultureInfo.InvariantCulture)) .StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir_2", "off", "on"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.True(called); } @@ -312,18 +279,14 @@ public async Task CallbackObserverAttributeMissingShouldReturnNull() string? missingString = "has initial value"; // ACT - DefaultDaemonRxApp.Entities(n => n.EntityId.StartsWith("binary_sensor.pir")) + DefaultDaemonRxApp.Entities(n => n.EntityId.StartsWith("binary_sensor.pir", true, CultureInfo.InvariantCulture)) .StateChanges - .Subscribe(s => - { - missingString = s.New.Attribute?.missing_attribute; - }); + .Subscribe(s => missingString = s.New.Attribute?.missing_attribute); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir_2", "off", "on"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.Null(missingString); } @@ -338,16 +301,12 @@ public async Task UsingEntitiesNewEventShouldCallFunction() // ACT DefaultDaemonRxApp.Entities("binary_sensor.pir", "binary_sensor.pir_2") .StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir_2", "off", "on"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.True(called); } @@ -362,16 +321,12 @@ public async Task UsingEntityNewEventShouldCallFunction() // ACT DefaultDaemonRxApp.Entity("binary_sensor.pir") .StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.True(called); } @@ -386,16 +341,12 @@ public async Task UsingEntityNewEventShouldNotCallFunction() // ACT DefaultDaemonRxApp.Entity("binary_sensor.other_pir") .StateChanges - .Subscribe(s => - { - called = true; - }); + .Subscribe(_ => called = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.False(called); } @@ -408,16 +359,12 @@ public async Task WhenStateStaysSameForTimeItShouldCallFunction() using var ctx = DefaultDaemonRxApp.StateChanges .Where(t => t.New.EntityId == "binary_sensor.pir") .NDSameStateFor(TimeSpan.FromMilliseconds(50)) - .Subscribe(e => - { - isRun = true; - }); + .Subscribe(_ => isRun = true); DefaultHassClientMock.AddChangedEvent("binary_sensor.pir", "off", "on"); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - Assert.True(isRun); } @@ -435,7 +382,6 @@ public async Task SavedDataShouldReturnSameDataUsingExpando() await RunFakeDaemonUntilTimeout().ConfigureAwait(false); - // ASSERT Assert.Equal(data, collectedData); } diff --git a/tests/NetDaemon.Daemon.Tests/Reactive/RxSchedulers.cs b/tests/NetDaemon.Daemon.Tests/Reactive/RxSchedulers.cs index 0737b6efe..bae74601f 100644 --- a/tests/NetDaemon.Daemon.Tests/Reactive/RxSchedulers.cs +++ b/tests/NetDaemon.Daemon.Tests/Reactive/RxSchedulers.cs @@ -1,4 +1,6 @@ using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Moq; @@ -16,22 +18,19 @@ namespace NetDaemon.Daemon.Tests.Reactive /// public class RxSchedulerTest : CoreDaemonHostTestBase { - public RxSchedulerTest() : base() - { - } - [Fact] + [SuppressMessage("", "CA2201")] public async Task CreateObservableIntervallFailureShouldLogError() { // ARRANGE - var app = new BaseTestRxApp(); + await using var app = new BaseTestRxApp(); await app.StartUpAsync(DefaultDaemonHost).ConfigureAwait(false); app.IsEnabled = true; // ACT using var disposable = app.CreateObservableIntervall(TimeSpan.FromMilliseconds(10), () => throw new Exception("hello!")); - await Task.Delay(150); + await Task.Delay(150).ConfigureAwait(false); // ASSERT LoggerMock.AssertLogged(LogLevel.Error, Times.AtLeastOnce()); } @@ -40,31 +39,34 @@ public async Task CreateObservableIntervallFailureShouldLogError() public async Task CreateObservableIntervallShouldCallFunction() { // ARRANGE - var app = new BaseTestRxApp(); - app.IsEnabled = true; + await using var app = new BaseTestRxApp + { + IsEnabled = true + }; var called = false; // ACT using var disposable = app.CreateObservableIntervall(TimeSpan.FromMilliseconds(10), () => called = true); - await Task.Delay(150); + await Task.Delay(150).ConfigureAwait(false); // ASSERT Assert.True(called); } [Fact] + [SuppressMessage("", "CA2201")] public async Task CreateObservableTimerFailureShouldLogError() { // ARRANGE - var app = new BaseTestRxApp(); + await using var app = new BaseTestRxApp(); await app.StartUpAsync(DefaultDaemonHost).ConfigureAwait(false); app.IsEnabled = true; // ACT using var disposable = app.CreateObservableTimer(DateTime.Now, TimeSpan.FromMilliseconds(10), () => throw new Exception("Hello!")); - await Task.Delay(150); + await Task.Delay(150).ConfigureAwait(false); // ASSERT LoggerMock.AssertLogged(LogLevel.Error, Times.AtLeastOnce()); } @@ -73,15 +75,17 @@ public async Task CreateObservableTimerFailureShouldLogError() public async Task CreateObservableTimerShouldCallFunction() { // ARRANGE - var app = new BaseTestRxApp(); - app.IsEnabled = true; + await using var app = new BaseTestRxApp + { + IsEnabled = true + }; var called = false; // ACT using var disposable = app.CreateObservableTimer(DateTime.Now, TimeSpan.FromMilliseconds(10), () => called = true); - await Task.Delay(100); + await Task.Delay(100).ConfigureAwait(false); // ASSERT Assert.True(called); } @@ -92,10 +96,10 @@ public void RunDailyOneHourAfterShouldCallCreateObservableIntervall() // ARRANGE var time = DateTime.Now; var timeOneHourBack = time.AddHours(1); - var timeFormat = timeOneHourBack.ToString("HH:mm:ss"); + var timeFormat = timeOneHourBack.ToString("HH:mm:ss", CultureInfo.InvariantCulture); // ACT - DefaultMockedRxApp.Object.RunDaily(timeFormat, () => System.Console.WriteLine("Test")); + DefaultMockedRxApp.Object.RunDaily(timeFormat, () => Console.WriteLine("Test")); // ASSERT DefaultMockedRxApp.Verify(n => n.CreateObservableTimer(It.IsAny(), TimeSpan.FromDays(1), It.IsAny()), Times.Once()); @@ -107,7 +111,7 @@ public void RunDailyOneHourBeforeShouldCallCreateObservableIntervall() // ARRANGE var time = DateTime.Now; var timeOneHourBack = time.Subtract(TimeSpan.FromHours(1)); - var timeFormat = timeOneHourBack.ToString("HH:mm:ss"); + var timeFormat = timeOneHourBack.ToString("HH:mm:ss", CultureInfo.InvariantCulture); // ACT DefaultMockedRxApp.Object.RunDaily(timeFormat, () => System.Console.WriteLine("Test")); @@ -122,7 +126,7 @@ public void RunDailyShouldCallCreateObservableIntervall() // ARRANGE // ACT - DefaultMockedRxApp.Object.RunDaily("10:00:00", () => System.Console.WriteLine("Test")); + DefaultMockedRxApp.Object.RunDaily("10:00:00", () => Console.WriteLine("Test")); // ASSERT DefaultMockedRxApp.Verify(n => n.CreateObservableTimer(It.IsAny(), TimeSpan.FromDays(1), It.IsAny()), Times.Once()); @@ -135,7 +139,7 @@ public void RunDailyShouldThrowExceptionOnErrorFormat() // ACT // ASSERT Assert.Throws(() => - DefaultMockedRxApp.Object.RunDaily("no good input", () => System.Console.WriteLine("Test"))); + DefaultMockedRxApp.Object.RunDaily("no good input", () => Console.WriteLine("Test"))); } [Fact] @@ -143,7 +147,7 @@ public void RunEveryHourShouldCallCreateObservableIntervall() { // ARRANGE // ACT - DefaultMockedRxApp.Object.RunEveryHour("10:00", () => System.Console.WriteLine("Test")); + DefaultMockedRxApp.Object.RunEveryHour("10:00", () => Console.WriteLine("Test")); // ASSERT DefaultMockedRxApp.Verify(n => n.CreateObservableTimer(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); @@ -156,7 +160,7 @@ public void RunEveryHourShouldThrowExceptionOnErrorFormat() // ACT // ASSERT Assert.Throws(() => - DefaultMockedRxApp.Object.RunEveryHour("no good input", () => System.Console.WriteLine("Test"))); + DefaultMockedRxApp.Object.RunEveryHour("no good input", () => Console.WriteLine("Test"))); } [Fact] @@ -164,7 +168,7 @@ public void RunEveryMinuteShouldCallCreateObservableIntervall() { // ARRANGE // ACT - DefaultMockedRxApp.Object.RunEveryMinute(0, () => System.Console.WriteLine("Test")); + DefaultMockedRxApp.Object.RunEveryMinute(0, () => Console.WriteLine("Test")); // ASSERT DefaultMockedRxApp.Verify(n => n.CreateObservableTimer(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); @@ -177,7 +181,7 @@ public void RunEveryMinuteShouldThrowExceptionOnErrorFormat() // ACT // ASSERT Assert.Throws(() => - DefaultMockedRxApp.Object.RunEveryMinute(-1, () => System.Console.WriteLine("Test"))); + DefaultMockedRxApp.Object.RunEveryMinute(-1, () => Console.WriteLine("Test"))); } [Fact] @@ -186,12 +190,13 @@ public void RunEveryShouldCallCreateObservableIntervall() // ARRANGE // ACT - DefaultMockedRxApp.Object.RunEvery(TimeSpan.FromSeconds(5), () => System.Console.WriteLine("Test")); + DefaultMockedRxApp.Object.RunEvery(TimeSpan.FromSeconds(5), () => Console.WriteLine("Test")); // ASSERT DefaultMockedRxApp.Verify(n => n.CreateObservableIntervall(TimeSpan.FromSeconds(5), It.IsAny()), Times.Once()); } [Fact] + [SuppressMessage("", "CA2201")] public async Task RunInFailureShouldLogError() { // ARRANGE @@ -200,7 +205,7 @@ public async Task RunInFailureShouldLogError() DefaultDaemonRxApp.RunIn(TimeSpan.FromMilliseconds(100), () => throw new Exception("RxError")); // ASSERT - await Task.Delay(150); + await Task.Delay(150).ConfigureAwait(false); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); LoggerMock.AssertLogged(LogLevel.Error, Times.Once()); } @@ -218,7 +223,7 @@ public async Task RunInShouldCallFunction() // ASSERT Assert.False(called); - await Task.Delay(150); + await Task.Delay(150).ConfigureAwait(false); await RunFakeDaemonUntilTimeout().ConfigureAwait(false); Assert.True(called); diff --git a/tests/NetDaemon.Daemon.Tests/SchedulerTests.cs b/tests/NetDaemon.Daemon.Tests/SchedulerTests.cs index d19caee0a..a4fb1f3c3 100644 --- a/tests/NetDaemon.Daemon.Tests/SchedulerTests.cs +++ b/tests/NetDaemon.Daemon.Tests/SchedulerTests.cs @@ -7,12 +7,14 @@ using NetDaemon.Common; using Xunit; using NetDaemon.Daemon.Fakes; +using System.Diagnostics.CodeAnalysis; namespace NetDaemon.Daemon.Tests { public class SchedulerTests { [Fact] + [SuppressMessage("", "CA1031")] public async void TestRunInShouldStartAndCompleteCorrectly() { // ARRANGE @@ -28,21 +30,21 @@ await using (IScheduler scheduler = new Scheduler()) scheduledResult = scheduler.RunIn(200, async () => { isTaskRun = true; - await Task.Delay(1); + await Task.Delay(1).ConfigureAwait(false); }); // ASSERT // Assert not run before time - await Task.Delay(100); + await Task.Delay(100).ConfigureAwait(false); Assert.False(isTaskRun); - await Task.Delay(150); + await Task.Delay(150).ConfigureAwait(false); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -53,6 +55,7 @@ await using (IScheduler scheduler = new Scheduler()) } [Fact] + [SuppressMessage("", "CA1031")] public async void RunInShouldLogWarningForFaultyRun() { // ARRANGE @@ -68,16 +71,16 @@ await using (IScheduler scheduler = new Scheduler(null, loggerMock.LoggerFactory // ACT scheduledResult = scheduler.RunIn(20, () => { - int i = int.Parse("Not an integer makes runtime error!"); + int i = int.Parse("Not an integer makes runtime error!", CultureInfo.InvariantCulture); return Task.CompletedTask; }); - await Task.Delay(100); + await Task.Delay(100).ConfigureAwait(false); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -87,6 +90,7 @@ await using (IScheduler scheduler = new Scheduler(null, loggerMock.LoggerFactory } [Fact] + [SuppressMessage("", "CA1031")] public async void TestRunInShouldStartAndAncCancelCorrectly() { // ARRANGE @@ -102,21 +106,21 @@ await using (IScheduler scheduler = new Scheduler()) scheduledResult = scheduler.RunIn(200, async () => { isTaskRun = true; - await Task.Delay(1); + await Task.Delay(1).ConfigureAwait(false); }); // ASSERT // Assert not run before time - await Task.Delay(100); + await Task.Delay(100).ConfigureAwait(false); Assert.False(isTaskRun); scheduledResult.CancelSource.Cancel(); - await Task.Delay(150); + await Task.Delay(150).ConfigureAwait(false); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -132,16 +136,16 @@ await using (IScheduler scheduler = new Scheduler()) [InlineData("00:00:00", "00:00:00", 0)] [InlineData("23:59:59", "00:00:00", 1)] [InlineData("00:00:01", "00:00:00", (24 * 60 * 60) - 1)] - public void DailyTimeBetweenNowAndTargetTime(string nowTime, string targetTime, int nrOfSecondsRemaining) + public async Task DailyTimeBetweenNowAndTargetTime(string nowTime, string targetTime, int nrOfSecondsRemaining) { // ARRANGE DateTime timePart = DateTime.ParseExact(nowTime, "HH:mm:ss", CultureInfo.InvariantCulture); - DateTime fakeTimeNow = new DateTime(2001, 01, 01, timePart.Hour, timePart.Minute, timePart.Second); + DateTime fakeTimeNow = new(2001, 01, 01, timePart.Hour, timePart.Minute, timePart.Second); DateTime timeTarget = DateTime.ParseExact(targetTime, "HH:mm:ss", CultureInfo.InvariantCulture); var mockTimeManager = new TimeManagerMock(fakeTimeNow); - var scheduler = new Scheduler(mockTimeManager.Object); + await using var scheduler = new Scheduler(mockTimeManager.Object); var timeToWait = scheduler.CalculateDailyTimeBetweenNowAndTargetTime(timeTarget); @@ -153,7 +157,7 @@ public void DailyTimeBetweenNowAndTargetTime(string nowTime, string targetTime, [InlineData(59, 0, 1)] [InlineData(0, 59, 59)] [InlineData(31, 30, 59)] - public void EveryMinuteCalcTimeCorrectTargetDelay(short nowSeconds, short targetSeconds, short expectedDelaySeconds) + public async Task EveryMinuteCalcTimeCorrectTargetDelay(short nowSeconds, short targetSeconds, short expectedDelaySeconds) { // ARRANGE var startTime = @@ -161,7 +165,7 @@ public void EveryMinuteCalcTimeCorrectTargetDelay(short nowSeconds, short target var mockTimeManager = new TimeManagerMock(startTime); - var scheduler = new Scheduler(mockTimeManager.Object); + await using var scheduler = new Scheduler(mockTimeManager.Object); var calculatedDelay = scheduler.CalculateEveryMinuteTimeBetweenNowAndTargetTime(targetSeconds); @@ -169,6 +173,8 @@ public void EveryMinuteCalcTimeCorrectTargetDelay(short nowSeconds, short target } [Fact] + [SuppressMessage("", "CA1508")] + [SuppressMessage("", "CA1031")] public async void TestRunDailyUsingStartTimeCallsFuncCorrectly() { // ARRANGE @@ -184,18 +190,18 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) scheduledResult = scheduler.RunDaily("10:00:01", async () => { nrOfRuns++; - await Task.Delay(1); + await Task.Delay(1).ConfigureAwait(false); }); - await Task.Delay(600); + await Task.Delay(600).ConfigureAwait(false); // ASSERT Assert.True(nrOfRuns == 0); - await Task.Delay(500); + await Task.Delay(500).ConfigureAwait(false); Assert.True(nrOfRuns == 1); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -203,6 +209,7 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) } [Fact] + [SuppressMessage("", "CA1031")] public async void RunDailyFaultShouldLogWarning() { // ARRANGE @@ -218,14 +225,14 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object, logger // ACT scheduledResult = scheduler.RunDaily("10:00:01", () => { - int i = int.Parse("Not an integer makes runtime error!"); + int i = int.Parse("Not an integer makes runtime error!", CultureInfo.InvariantCulture); return Task.CompletedTask; }); - await Task.Delay(1500); + await Task.Delay(1500).ConfigureAwait(false); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -236,6 +243,7 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object, logger } [Fact] + [SuppressMessage("", "CA1031")] public async void RunDailyOnDaysFaultShouldLogWarning() { // ARRANGE @@ -251,14 +259,14 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object, logger // ACT scheduledResult = scheduler.RunDaily("10:00:01", new DayOfWeek[] { DayOfWeek.Saturday }, () => { - int i = int.Parse("Not an integer makes runtime error!"); + int i = int.Parse("Not an integer makes runtime error!", CultureInfo.InvariantCulture); return Task.CompletedTask; }); - await Task.Delay(1500); + await Task.Delay(1500).ConfigureAwait(false); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -269,6 +277,8 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object, logger } [Fact] + [SuppressMessage("", "CA1508")] + [SuppressMessage("", "CA1031")] public async void TestRunDailyUsingStartTimeCancelsCorrectly() { // ARRANGE @@ -284,19 +294,19 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) scheduledResult = scheduler.RunDaily("10:00:01", async () => { nrOfRuns++; - await Task.Delay(1); + await Task.Delay(1).ConfigureAwait(false); }); - await Task.Delay(600); + await Task.Delay(600).ConfigureAwait(false); // ASSERT Assert.True(nrOfRuns == 0); scheduledResult.CancelSource.Cancel(); - await Task.Delay(500); + await Task.Delay(500).ConfigureAwait(false); Assert.True(nrOfRuns == 0); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -312,6 +322,8 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) [InlineData("2001-02-07 10:00:00", DayOfWeek.Wednesday)] [InlineData("2001-02-08 10:00:00", DayOfWeek.Thursday)] [InlineData("2001-02-09 10:00:00", DayOfWeek.Friday)] + [SuppressMessage("", "CA1508")] + [SuppressMessage("", "CA1031")] public async void TestRunDailyUsingStartTimeOnWeekdayCallsFuncCorrectly(string time, DayOfWeek dayOfWeek) { // ARRANGE @@ -327,19 +339,19 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) scheduledResult = scheduler.RunDaily("10:00:01", new DayOfWeek[] { dayOfWeek }, async () => { nrOfRuns++; - await Task.Delay(1); + await Task.Delay(1).ConfigureAwait(false); }); - await Task.Delay(600); + await Task.Delay(600).ConfigureAwait(false); // ASSERT Assert.True(nrOfRuns == 0); - await Task.Delay(800); + await Task.Delay(800).ConfigureAwait(false); Assert.True(nrOfRuns >= 1); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -353,6 +365,8 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) [InlineData("2001-02-07 10:00:00")] [InlineData("2001-02-08 10:00:00")] [InlineData("2001-02-09 10:00:00")] + [SuppressMessage("", "CA1508")] + [SuppressMessage("", "CA1031")] public async void TestRunDailyUsingStartTimeOnWeekdayNotCalled(string time) { // ARRANGE @@ -369,19 +383,19 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) scheduledResult = scheduler.RunDaily("10:00:01", new DayOfWeek[] { DayOfWeek.Monday }, async () => { nrOfRuns++; - await Task.Delay(1); + await Task.Delay(1).ConfigureAwait(false); }); - await Task.Delay(600); + await Task.Delay(600).ConfigureAwait(false); // ASSERT Assert.True(nrOfRuns == 0); - await Task.Delay(500); + await Task.Delay(500).ConfigureAwait(false); Assert.False(nrOfRuns == 1); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -389,6 +403,7 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) } [Fact] + [SuppressMessage("", "CA1031")] public async void TestRunEveryMinuteStartTimeCallsFuncCorrectly() { // ARRANGE @@ -405,19 +420,19 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) scheduledResult = scheduler.RunEveryMinute(0, async () => { nrOfRuns++; - await Task.Delay(1); + await Task.Delay(1).ConfigureAwait(false); }); - await Task.Delay(300); + await Task.Delay(300).ConfigureAwait(false); // ASSERT Assert.Equal(0, nrOfRuns); - await Task.Delay(1500); + await Task.Delay(1500).ConfigureAwait(false); Assert.True(nrOfRuns >= 1); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -425,6 +440,7 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) } [Fact] + [SuppressMessage("", "CA1031")] public async void RunEveryMinuteFaultyShouldLogWarning() { // ARRANGE @@ -440,15 +456,15 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object, logger // ACT scheduledResult = scheduler.RunEveryMinute(0, () => { - int i = int.Parse("Not an integer makes runtime error!"); + int i = int.Parse("Not an integer makes runtime error!", CultureInfo.InvariantCulture); return Task.CompletedTask; }); - await Task.Delay(1000); + await Task.Delay(1000).ConfigureAwait(false); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -458,6 +474,7 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object, logger } [Fact] + [SuppressMessage("", "CA1031")] public async void TestRunEveryMinuteStartTimeCanceledCorrectly() { // ARRANGE @@ -474,20 +491,20 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) scheduledResult = scheduler.RunEveryMinute(0, async () => { nrOfRuns++; - await Task.Delay(1); + await Task.Delay(1).ConfigureAwait(false); }); - await Task.Delay(300); + await Task.Delay(300).ConfigureAwait(false); // ASSERT Assert.Equal(0, nrOfRuns); scheduledResult.CancelSource.Cancel(); - await Task.Delay(1500); + await Task.Delay(1500).ConfigureAwait(false); Assert.Equal(0, nrOfRuns); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -496,6 +513,8 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) } [Fact] + [SuppressMessage("", "CA1508")] + [SuppressMessage("", "CA1031")] public async void TestRunEveryMinuteStartTimeNotZeroCallsFuncCorrectly() { // ARRANGE @@ -513,19 +532,19 @@ await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) scheduledResult = scheduler.RunEveryMinute(20, async () => { nrOfRuns++; - await Task.Delay(1); + await Task.Delay(1).ConfigureAwait(false); }); - await Task.Delay(600); + await Task.Delay(600).ConfigureAwait(false); // ASSERT Assert.True(nrOfRuns == 0); - await Task.Delay(500); + await Task.Delay(500).ConfigureAwait(false); Assert.True(nrOfRuns == 1); } try { - await scheduledResult.Task; + await scheduledResult.Task.ConfigureAwait(false); } catch { @@ -542,12 +561,9 @@ public async Task ScheduleLongTaskWillCompensateTimeToZero() await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) { // ACT - var runTask = scheduler.RunEvery(20, async () => - { - await Task.Delay(50); - }); + var runTask = scheduler.RunEvery(20, async () => await Task.Delay(50).ConfigureAwait(false)); - await Task.WhenAny(runTask.Task, Task.Delay(500)); + await Task.WhenAny(runTask.Task, Task.Delay(500)).ConfigureAwait(false); } // ASSERT mockTimeManager.Verify(n => n.Delay(It.IsAny(), It.IsAny()), Times.Never); @@ -563,12 +579,9 @@ public async Task ScheduleLongTaskWillCompensateTime() await using (IScheduler scheduler = new Scheduler(mockTimeManager.Object)) { // ACT - var runTask = scheduler.RunEvery(20, async () => - { - await Task.Delay(1); - }); + var runTask = scheduler.RunEvery(20, async () => await Task.Delay(1).ConfigureAwait(false)); - await Task.WhenAny(runTask.Task, Task.Delay(100)); + await Task.WhenAny(runTask.Task, Task.Delay(100)).ConfigureAwait(false); } // ASSERT diff --git a/tests/NetDaemon.Daemon.Tests/TestBaseClasses.cs b/tests/NetDaemon.Daemon.Tests/TestBaseClasses.cs index c5f5a7e32..dcdce4611 100644 --- a/tests/NetDaemon.Daemon.Tests/TestBaseClasses.cs +++ b/tests/NetDaemon.Daemon.Tests/TestBaseClasses.cs @@ -16,32 +16,33 @@ public class BaseTestApp : Common.NetDaemonApp { } public class BaseTestRxApp : NetDaemonRxApp { } - public class CoreDaemonHostTestBase : DaemonHostTestBase, IAsyncLifetime + public class CoreDaemonHostTestBase : DaemonHostTestBase, IAsyncLifetime, IDisposable { - private readonly Common.NetDaemonApp _defaultDaemonApp; - private readonly BaseTestRxApp _defaultDaemonRxApp; - private readonly Mock _defaultMockedRxApp; - private readonly NetDaemonHost _notConnectedDaemonHost; + private bool disposedValue; public CoreDaemonHostTestBase() : base() { - _defaultDaemonApp = new BaseTestApp(); - _defaultDaemonApp.Id = "app_id"; - _defaultDaemonApp.IsEnabled = true; + DefaultDaemonApp = new BaseTestApp + { + Id = "app_id", + IsEnabled = true + }; - DefaultDaemonHost.InternalRunningAppInstances[_defaultDaemonApp.Id!] = _defaultDaemonApp; + DefaultDaemonHost.InternalRunningAppInstances[DefaultDaemonApp.Id!] = DefaultDaemonApp; - _defaultDaemonRxApp = new BaseTestRxApp(); - _defaultDaemonRxApp.Id = "app_rx_id"; - _defaultDaemonRxApp.IsEnabled = true; - DefaultDaemonHost.InternalRunningAppInstances[_defaultDaemonRxApp.Id!] = _defaultDaemonRxApp; + DefaultDaemonRxApp = new BaseTestRxApp + { + Id = "app_rx_id", + 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; + 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(HassClientMock.MockConnectFalse.Object, DefaultDataRepositoryMock.Object, LoggerMock.LoggerFactory); @@ -71,7 +72,6 @@ public void SetupFakeData() State = "off" }); - SetEntityState(new() { EntityId = "switch.correct_entity", @@ -119,9 +119,9 @@ public void SetupFakeData() } }); - SetEntityState( new() + SetEntityState(new() { - EntityId = "light.ligth_in_area", + EntityId = "light.light_in_area", State = "off", Attributes = new Dictionary { @@ -137,9 +137,11 @@ public new async Task DisposeAsync() { await base.DisposeAsync().ConfigureAwait(false); - await _defaultDaemonApp.DisposeAsync().ConfigureAwait(false); - await _defaultDaemonRxApp.DisposeAsync().ConfigureAwait(false); - await _defaultMockedRxApp.Object.DisposeAsync().ConfigureAwait(false); + await _notConnectedDaemonHost.DisposeAsync().ConfigureAwait(false); + await DefaultDaemonApp.DisposeAsync().ConfigureAwait(false); + await DefaultDaemonRxApp.DisposeAsync().ConfigureAwait(false); + await DefaultMockedRxApp.Object.DisposeAsync().ConfigureAwait(false); + await DefaultDaemonRxApp.DisposeAsync().ConfigureAwait(false); } /// @@ -149,15 +151,15 @@ public new async Task InitializeAsync() { await base.InitializeAsync().ConfigureAwait(false); - await _defaultDaemonApp.StartUpAsync(DefaultDaemonHost).ConfigureAwait(false); - await _defaultDaemonRxApp.StartUpAsync(DefaultDaemonHost).ConfigureAwait(false); - await _defaultMockedRxApp.Object.StartUpAsync(DefaultDaemonHost).ConfigureAwait(false); + await DefaultDaemonApp.StartUpAsync(DefaultDaemonHost).ConfigureAwait(false); + await DefaultDaemonRxApp.StartUpAsync(DefaultDaemonHost).ConfigureAwait(false); + await DefaultMockedRxApp.Object.StartUpAsync(DefaultDaemonHost).ConfigureAwait(false); } - public BaseTestRxApp DefaultDaemonRxApp => _defaultDaemonRxApp; - public Mock DefaultMockedRxApp => _defaultMockedRxApp; - public Common.NetDaemonApp DefaultDaemonApp => _defaultDaemonApp; - public string HelloWorldData => "Hello world!"; + public BaseTestRxApp DefaultDaemonRxApp { get; } + public Mock DefaultMockedRxApp { get; } + public Common.NetDaemonApp DefaultDaemonApp { get; } + public static string HelloWorldData => "Hello world!"; public (Task, CancellationTokenSource) ReturnRunningNotConnectedDaemonHostTask(short milliSeconds = 100, bool overrideDebugNotCancel = false) { @@ -167,6 +169,22 @@ public new async Task InitializeAsync() return (_notConnectedDaemonHost.Run("host", 8123, false, "token", cancelSource.Token), cancelSource); } + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + } + disposedValue = true; + } + } + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } \ No newline at end of file diff --git a/tests/NetDaemon.Daemon.Tests/TimeManagerMock.cs b/tests/NetDaemon.Daemon.Tests/TimeManagerMock.cs index 97f5c1d9a..ae2c8d5b7 100644 --- a/tests/NetDaemon.Daemon.Tests/TimeManagerMock.cs +++ b/tests/NetDaemon.Daemon.Tests/TimeManagerMock.cs @@ -14,14 +14,14 @@ public TimeManagerMock(DateTime time) _time = time; SetupGet(n => n.Current).Returns(_time); Setup(n => n.Delay(It.IsAny(), It.IsAny())) - .Returns(async (TimeSpan s, CancellationToken c) => await Task.Delay(s, c)); + .Returns(async (TimeSpan s, CancellationToken c) => await Task.Delay(s, c).ConfigureAwait(false)); } public TimeManagerMock() { SetupGet(n => n.Current).Returns(_time); Setup(n => n.Delay(It.IsAny(), It.IsAny())) - .Returns(async (TimeSpan s, CancellationToken c) => await Task.Delay(s, c)); + .Returns(async (TimeSpan s, CancellationToken c) => await Task.Delay(s, c).ConfigureAwait(false)); } } } \ No newline at end of file