From 24df4e7a033c665de7043750328bc373d21c7004 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 12:41:15 -0600 Subject: [PATCH] feat: Add unit tests and fakes for Crestron environment and data store - Introduced `FakeCrestronEnvironment` and `FakeEthernetHelper` for testing purposes. - Implemented `InMemoryCrestronDataStore` to facilitate unit tests for data storage. - Created `DebugServiceTests` to validate the behavior of debug-related services. - Added project files for unit tests targeting .NET 9.0 with necessary dependencies. - Developed production adapters (`CrestronConsoleAdapter`, `CrestronDataStoreAdapter`, `CrestronEnvironmentAdapter`, `CrestronEthernetAdapter`) to interface with the Crestron SDK. - Updated `Debug` class to utilize injected service abstractions instead of direct SDK calls. - Enhanced `ControlSystem` initialization to register service adapters before usage. --- .../DebugServiceRegistration.cs | 36 ++++ src/PepperDash.Core.Abstractions/Enums.cs | 100 +++++++++ .../ICrestronConsole.cs | 32 +++ .../ICrestronDataStore.cs | 31 +++ .../ICrestronEnvironment.cs | 45 ++++ .../IEthernetHelper.cs | 16 ++ .../PepperDash.Core.Abstractions.csproj | 19 ++ .../Fakes/CrestronConsoleFakes.cs | 38 ++++ .../Fakes/CrestronEnvironmentFakes.cs | 46 ++++ .../Fakes/InMemoryCrestronDataStore.cs | 59 ++++++ .../Logging/DebugServiceTests.cs | 171 +++++++++++++++ .../PepperDash.Core.Tests.csproj | 25 +++ .../Adapters/CrestronConsoleAdapter.cs | 35 ++++ .../Adapters/CrestronDataStoreAdapter.cs | 42 ++++ .../Adapters/CrestronEnvironmentAdapter.cs | 68 ++++++ .../Adapters/CrestronEthernetAdapter.cs | 39 ++++ src/PepperDash.Core/Logging/Debug.cs | 198 ++++++++++-------- src/PepperDash.Core/PepperDash.Core.csproj | 3 + .../Config/ConfigServiceFakesTests.cs | 73 +++++++ .../PepperDash.Essentials.Core.Tests.csproj | 27 +++ src/PepperDash.Essentials/ControlSystem.cs | 10 + 21 files changed, 1020 insertions(+), 93 deletions(-) create mode 100644 src/PepperDash.Core.Abstractions/DebugServiceRegistration.cs create mode 100644 src/PepperDash.Core.Abstractions/Enums.cs create mode 100644 src/PepperDash.Core.Abstractions/ICrestronConsole.cs create mode 100644 src/PepperDash.Core.Abstractions/ICrestronDataStore.cs create mode 100644 src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs create mode 100644 src/PepperDash.Core.Abstractions/IEthernetHelper.cs create mode 100644 src/PepperDash.Core.Abstractions/PepperDash.Core.Abstractions.csproj create mode 100644 src/PepperDash.Core.Tests/Fakes/CrestronConsoleFakes.cs create mode 100644 src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs create mode 100644 src/PepperDash.Core.Tests/Fakes/InMemoryCrestronDataStore.cs create mode 100644 src/PepperDash.Core.Tests/Logging/DebugServiceTests.cs create mode 100644 src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj create mode 100644 src/PepperDash.Core/Adapters/CrestronConsoleAdapter.cs create mode 100644 src/PepperDash.Core/Adapters/CrestronDataStoreAdapter.cs create mode 100644 src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs create mode 100644 src/PepperDash.Core/Adapters/CrestronEthernetAdapter.cs create mode 100644 src/PepperDash.Essentials.Core.Tests/Config/ConfigServiceFakesTests.cs create mode 100644 src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj diff --git a/src/PepperDash.Core.Abstractions/DebugServiceRegistration.cs b/src/PepperDash.Core.Abstractions/DebugServiceRegistration.cs new file mode 100644 index 00000000..c5e18c5a --- /dev/null +++ b/src/PepperDash.Core.Abstractions/DebugServiceRegistration.cs @@ -0,0 +1,36 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Allows pre-registration of Crestron service implementations before the Debug +/// static class initialises. Call from the composition root +/// (e.g. ControlSystem constructor) before any code touches Debug.*. +/// Test projects should call it with no-op / in-memory implementations so that the +/// Debug static constructor never tries to reach the real Crestron SDK. +/// +public static class DebugServiceRegistration +{ + /// Gets the registered environment abstraction, or null if not registered. + public static ICrestronEnvironment? Environment { get; private set; } + + /// Gets the registered console abstraction, or null if not registered. + public static ICrestronConsole? Console { get; private set; } + + /// Gets the registered data-store abstraction, or null if not registered. + public static ICrestronDataStore? DataStore { get; private set; } + + /// + /// Registers the service implementations that Debug will use when its + /// static constructor runs. Any parameter may be null to leave the + /// corresponding service unregistered (the Debug class will skip that + /// capability gracefully). + /// + public static void Register( + ICrestronEnvironment? environment, + ICrestronConsole? console, + ICrestronDataStore? dataStore) + { + Environment = environment; + Console = console; + DataStore = dataStore; + } +} diff --git a/src/PepperDash.Core.Abstractions/Enums.cs b/src/PepperDash.Core.Abstractions/Enums.cs new file mode 100644 index 00000000..cd6052e6 --- /dev/null +++ b/src/PepperDash.Core.Abstractions/Enums.cs @@ -0,0 +1,100 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Mirrors Crestron's eDevicePlatform without requiring the Crestron SDK. +/// +public enum DevicePlatform +{ + /// Hardware appliance (e.g. CP4, MC4). + Appliance, + /// Crestron Virtual Control / server runtime. + Server, +} + +/// +/// Mirrors Crestron's eRuntimeEnvironment. +/// +public enum RuntimeEnvironment +{ + /// SimplSharpPro program slot (hardware 4-series). + SimplSharpPro, + /// SimplSharp (older 3-series or server environments). + SimplSharp, + /// Any other environment — check for completeness. + Other, +} + +/// +/// Mirrors Crestron's ConsoleAccessLevelEnum. +/// +public enum ConsoleAccessLevel +{ + AccessAdministrator = 0, + AccessOperator = 1, + AccessProgrammer = 2, +} + +/// +/// Mirrors Crestron's eProgramStatusEventType. +/// +public enum ProgramStatusEventType +{ + Starting, + Stopping, + Paused, + Resumed, +} + +/// +/// Mirrors the event type used by Crestron's EthernetEventArgs. +/// +public enum EthernetEventType +{ + LinkDown = 0, + LinkUp = 1, +} + +/// +/// Event args for Crestron ethernet link events. +/// +public class PepperDashEthernetEventArgs : EventArgs +{ + public EthernetEventType EthernetEventType { get; } + public short EthernetAdapter { get; } + + public PepperDashEthernetEventArgs(EthernetEventType eventType, short adapter) + { + EthernetEventType = eventType; + EthernetAdapter = adapter; + } +} + +/// +/// Mirrors the set of CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET values +/// used across this codebase — does not aim to be exhaustive. +/// +public enum EthernetParameterType +{ + GetCurrentIpAddress, + GetHostname, + GetDomainName, + GetLinkStatus, + GetCurrentDhcpState, + GetCurrentIpMask, + GetCurrentRouter, + GetMacAddress, + GetDnsServer, +} + +/// +/// Mirrors Crestron's SocketStatus without requiring the Crestron SDK. +/// +public enum PepperDashSocketStatus +{ + SocketNotConnected = 0, + SocketConnected = 2, + SocketConnectionInProgress = 6, + SocketConnectFailed = 11, + SocketDisconnecting = 12, + SocketBrokenRemotely = 7, +} diff --git a/src/PepperDash.Core.Abstractions/ICrestronConsole.cs b/src/PepperDash.Core.Abstractions/ICrestronConsole.cs new file mode 100644 index 00000000..335c4ee0 --- /dev/null +++ b/src/PepperDash.Core.Abstractions/ICrestronConsole.cs @@ -0,0 +1,32 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Abstracts Crestron.SimplSharp.CrestronConsole to allow unit testing +/// without the Crestron SDK. +/// +public interface ICrestronConsole +{ + /// Prints a line to the Crestron console/telnet output. + void PrintLine(string message); + + /// Prints text (without newline) to the Crestron console/telnet output. + void Print(string message); + + /// + /// Sends a response string to the console for the currently-executing console command. + /// + void ConsoleCommandResponse(string message); + + /// + /// Registers a new command with the Crestron console. + /// + /// Handler invoked when the command is typed. + /// Command name (no spaces). + /// Help text shown by the Crestron console. + /// Minimum access level required to run the command. + void AddNewConsoleCommand( + Action callback, + string command, + string helpText, + ConsoleAccessLevel accessLevel); +} diff --git a/src/PepperDash.Core.Abstractions/ICrestronDataStore.cs b/src/PepperDash.Core.Abstractions/ICrestronDataStore.cs new file mode 100644 index 00000000..96fd1856 --- /dev/null +++ b/src/PepperDash.Core.Abstractions/ICrestronDataStore.cs @@ -0,0 +1,31 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Abstracts Crestron.SimplSharp.CrestronDataStore.CrestronDataStoreStatic +/// to allow unit testing without the Crestron SDK. +/// +public interface ICrestronDataStore +{ + /// Initialises the data store. Must be called once before any other operation. + void InitStore(); + + /// Reads an integer value from the local (program-slot) store. + /// true if the value was found and read successfully. + bool TryGetLocalInt(string key, out int value); + + /// Writes an integer value to the local (program-slot) store. + /// true on success. + bool SetLocalInt(string key, int value); + + /// Writes an unsigned integer value to the local (program-slot) store. + /// true on success. + bool SetLocalUint(string key, uint value); + + /// Reads a boolean value from the local (program-slot) store. + /// true if the value was found and read successfully. + bool TryGetLocalBool(string key, out bool value); + + /// Writes a boolean value to the local (program-slot) store. + /// true on success. + bool SetLocalBool(string key, bool value); +} diff --git a/src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs b/src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs new file mode 100644 index 00000000..c31fb3f6 --- /dev/null +++ b/src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs @@ -0,0 +1,45 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Abstracts Crestron.SimplSharp.CrestronEnvironment to allow unit testing +/// without the Crestron SDK. +/// +public interface ICrestronEnvironment +{ + /// Gets the platform the program is executing on. + DevicePlatform DevicePlatform { get; } + + /// Gets the current runtime environment. + RuntimeEnvironment RuntimeEnvironment { get; } + + /// Gets the platform-appropriate newline string. + string NewLine { get; } + + /// Gets the application number (program slot). + uint ApplicationNumber { get; } + + /// Gets the room ID (used in Crestron Virtual Control / server environments). + uint RoomId { get; } + + /// Raised when program status changes (starting, stopping, etc.). + event EventHandler ProgramStatusChanged; + + /// Raised when the ethernet link changes state. + event EventHandler EthernetEventReceived; + + /// Gets the application root directory path. + string GetApplicationRootDirectory(); +} + +/// +/// Event args for . +/// +public class ProgramStatusEventArgs : EventArgs +{ + public ProgramStatusEventType EventType { get; } + + public ProgramStatusEventArgs(ProgramStatusEventType eventType) + { + EventType = eventType; + } +} diff --git a/src/PepperDash.Core.Abstractions/IEthernetHelper.cs b/src/PepperDash.Core.Abstractions/IEthernetHelper.cs new file mode 100644 index 00000000..0ae1c83b --- /dev/null +++ b/src/PepperDash.Core.Abstractions/IEthernetHelper.cs @@ -0,0 +1,16 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Abstracts Crestron.SimplSharp.CrestronEthernetHelper to allow unit testing +/// without the Crestron SDK. +/// +public interface IEthernetHelper +{ + /// + /// Returns a network parameter string for the specified adapter. + /// + /// The parameter to retrieve. + /// Ethernet adapter index (0 = LAN A). + /// String value of the requested parameter. + string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId); +} diff --git a/src/PepperDash.Core.Abstractions/PepperDash.Core.Abstractions.csproj b/src/PepperDash.Core.Abstractions/PepperDash.Core.Abstractions.csproj new file mode 100644 index 00000000..c1cdf5bc --- /dev/null +++ b/src/PepperDash.Core.Abstractions/PepperDash.Core.Abstractions.csproj @@ -0,0 +1,19 @@ + + + PepperDash.Core.Abstractions + PepperDash.Core.Abstractions + net8.0 + enable + true + en + PepperDash Core Abstractions + PepperDash Technologies + git + https://github.com/PepperDash/PepperDashCore + enable + false + $(Version) + true + + + diff --git a/src/PepperDash.Core.Tests/Fakes/CrestronConsoleFakes.cs b/src/PepperDash.Core.Tests/Fakes/CrestronConsoleFakes.cs new file mode 100644 index 00000000..afd5c324 --- /dev/null +++ b/src/PepperDash.Core.Tests/Fakes/CrestronConsoleFakes.cs @@ -0,0 +1,38 @@ +using PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Tests.Fakes; + +/// +/// No-op ICrestronConsole that captures output for test assertions. +/// +public class CapturingCrestronConsole : ICrestronConsole +{ + public List Lines { get; } = new(); + public List CommandResponses { get; } = new(); + public List<(string Command, string HelpText)> RegisteredCommands { get; } = new(); + + public void PrintLine(string message) => Lines.Add(message); + public void Print(string message) => Lines.Add(message); + public void ConsoleCommandResponse(string message) => CommandResponses.Add(message); + + public void AddNewConsoleCommand( + Action callback, + string command, + string helpText, + ConsoleAccessLevel accessLevel) + { + RegisteredCommands.Add((command, helpText)); + } +} + +/// +/// Minimal no-op ICrestronConsole that discards all output. Useful when you only +/// care about the system under test and not what it logs. +/// +public class NoOpCrestronConsole : ICrestronConsole +{ + public void PrintLine(string message) { } + public void Print(string message) { } + public void ConsoleCommandResponse(string message) { } + public void AddNewConsoleCommand(Action _, string __, string ___, ConsoleAccessLevel ____) { } +} diff --git a/src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs b/src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs new file mode 100644 index 00000000..8070f849 --- /dev/null +++ b/src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs @@ -0,0 +1,46 @@ +using PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Tests.Fakes; + +/// +/// Configurable ICrestronEnvironment for unit tests. +/// Defaults: Appliance / SimplSharpPro / ApplicationNumber=1. +/// +public class FakeCrestronEnvironment : ICrestronEnvironment +{ + public DevicePlatform DevicePlatform { get; set; } = DevicePlatform.Appliance; + public RuntimeEnvironment RuntimeEnvironment { get; set; } = RuntimeEnvironment.SimplSharpPro; + public string NewLine { get; set; } = "\r\n"; + public uint ApplicationNumber { get; set; } = 1; + public uint RoomId { get; set; } = 0; + + public event EventHandler? ProgramStatusChanged; + public event EventHandler? EthernetEventReceived; + + public string GetApplicationRootDirectory() => System.IO.Path.GetTempPath(); + + /// Simulates a program status event for tests. + public void RaiseProgramStatus(ProgramStatusEventType type) => + ProgramStatusChanged?.Invoke(this, new ProgramStatusEventArgs(type)); + + /// Simulates an ethernet event for tests. + public void RaiseEthernetEvent(EthernetEventType type, short adapter = 0) => + EthernetEventReceived?.Invoke(this, new PepperDashEthernetEventArgs(type, adapter)); +} + +/// +/// No-op IEthernetHelper that returns configurable values. +/// +public class FakeEthernetHelper : IEthernetHelper +{ + private readonly Dictionary _values = new(); + + public FakeEthernetHelper Seed(EthernetParameterType param, string value) + { + _values[param] = value; + return this; + } + + public string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId) => + _values.TryGetValue(parameter, out var v) ? v : string.Empty; +} diff --git a/src/PepperDash.Core.Tests/Fakes/InMemoryCrestronDataStore.cs b/src/PepperDash.Core.Tests/Fakes/InMemoryCrestronDataStore.cs new file mode 100644 index 00000000..7cc2b618 --- /dev/null +++ b/src/PepperDash.Core.Tests/Fakes/InMemoryCrestronDataStore.cs @@ -0,0 +1,59 @@ +using PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Tests.Fakes; + +/// +/// In-memory ICrestronDataStore backed by a dictionary. +/// Use in unit tests to verify that keys are read from and written to the store correctly. +/// +public class InMemoryCrestronDataStore : ICrestronDataStore +{ + private readonly Dictionary _store = new(); + + public bool Initialized { get; private set; } + + public void InitStore() => Initialized = true; + + public bool TryGetLocalInt(string key, out int value) + { + if (_store.TryGetValue(key, out var raw) && raw is int i) + { + value = i; + return true; + } + value = 0; + return false; + } + + public bool SetLocalInt(string key, int value) + { + _store[key] = value; + return true; + } + + public bool SetLocalUint(string key, uint value) + { + _store[key] = (int)value; + return true; + } + + public bool TryGetLocalBool(string key, out bool value) + { + if (_store.TryGetValue(key, out var raw) && raw is bool b) + { + value = b; + return true; + } + value = false; + return false; + } + + public bool SetLocalBool(string key, bool value) + { + _store[key] = value; + return true; + } + + /// Seeds a key for testing read paths. + public void Seed(string key, object value) => _store[key] = value; +} diff --git a/src/PepperDash.Core.Tests/Logging/DebugServiceTests.cs b/src/PepperDash.Core.Tests/Logging/DebugServiceTests.cs new file mode 100644 index 00000000..13ae0f9e --- /dev/null +++ b/src/PepperDash.Core.Tests/Logging/DebugServiceTests.cs @@ -0,0 +1,171 @@ +using FluentAssertions; +using PepperDash.Core.Abstractions; +using PepperDash.Core.Tests.Fakes; +using Xunit; + +namespace PepperDash.Core.Tests.Logging; + +/// +/// Tests for Debug-related service interfaces and implementations. +/// These tests verify the behaviour of the abstractions in isolation (no Crestron SDK required). +/// +public class DebugServiceTests +{ + // ----------------------------------------------------------------------- + // ICrestronDataStore — InMemoryCrestronDataStore + // ----------------------------------------------------------------------- + + [Fact] + public void DataStore_InitStore_SetsInitializedFlag() + { + var store = new InMemoryCrestronDataStore(); + store.Initialized.Should().BeFalse("not yet initialized"); + + store.InitStore(); + + store.Initialized.Should().BeTrue(); + } + + [Fact] + public void DataStore_SetAndGetLocalInt_RoundTrips() + { + var store = new InMemoryCrestronDataStore(); + + store.SetLocalInt("MyKey", 42).Should().BeTrue(); + store.TryGetLocalInt("MyKey", out var value).Should().BeTrue(); + value.Should().Be(42); + } + + [Fact] + public void DataStore_TryGetLocalInt_ReturnsFalse_WhenKeyAbsent() + { + var store = new InMemoryCrestronDataStore(); + + store.TryGetLocalInt("Missing", out var value).Should().BeFalse(); + value.Should().Be(0); + } + + [Fact] + public void DataStore_SetAndGetLocalBool_RoundTrips() + { + var store = new InMemoryCrestronDataStore(); + + store.SetLocalBool("FlagKey", true).Should().BeTrue(); + store.TryGetLocalBool("FlagKey", out var value).Should().BeTrue(); + value.Should().BeTrue(); + } + + [Fact] + public void DataStore_TryGetLocalBool_ReturnsFalse_WhenKeyAbsent() + { + var store = new InMemoryCrestronDataStore(); + + store.TryGetLocalBool("Missing", out var value).Should().BeFalse(); + value.Should().BeFalse(); + } + + [Fact] + public void DataStore_SetLocalUint_CanBeReadBackAsInt() + { + var store = new InMemoryCrestronDataStore(); + + store.SetLocalUint("UintKey", 3u).Should().BeTrue(); + store.TryGetLocalInt("UintKey", out var value).Should().BeTrue(); + value.Should().Be(3); + } + + [Fact] + public void DataStore_Seed_AllowsTestSetupOfReadPaths() + { + var store = new InMemoryCrestronDataStore(); + store.Seed("MyLevel", 2); + + store.TryGetLocalInt("MyLevel", out var level).Should().BeTrue(); + level.Should().Be(2); + } + + // ----------------------------------------------------------------------- + // DebugServiceRegistration + // ----------------------------------------------------------------------- + + [Fact] + public void ServiceRegistration_Register_StoresAllThreeServices() + { + var env = new FakeCrestronEnvironment(); + var console = new NoOpCrestronConsole(); + var store = new InMemoryCrestronDataStore(); + + DebugServiceRegistration.Register(env, console, store); + + DebugServiceRegistration.Environment.Should().BeSameAs(env); + DebugServiceRegistration.Console.Should().BeSameAs(console); + DebugServiceRegistration.DataStore.Should().BeSameAs(store); + } + + [Fact] + public void ServiceRegistration_Register_AcceptsNullsWithoutThrowing() + { + var act = () => DebugServiceRegistration.Register(null, null, null); + act.Should().NotThrow(); + } + + // ----------------------------------------------------------------------- + // ICrestronEnvironment — FakeCrestronEnvironment + // ----------------------------------------------------------------------- + + [Fact] + public void FakeEnvironment_DefaultsToAppliance() + { + var env = new FakeCrestronEnvironment(); + env.DevicePlatform.Should().Be(DevicePlatform.Appliance); + } + + [Fact] + public void FakeEnvironment_RaiseProgramStatus_FiresEvent() + { + var env = new FakeCrestronEnvironment(); + ProgramStatusEventType? received = null; + env.ProgramStatusChanged += (_, e) => received = e.EventType; + + env.RaiseProgramStatus(ProgramStatusEventType.Stopping); + + received.Should().Be(ProgramStatusEventType.Stopping); + } + + [Fact] + public void FakeEnvironment_RaiseEthernetEvent_FiresEvent() + { + var env = new FakeCrestronEnvironment(); + EthernetEventType? received = null; + env.EthernetEventReceived += (_, e) => received = e.EthernetEventType; + + env.RaiseEthernetEvent(EthernetEventType.LinkUp, adapter: 0); + + received.Should().Be(EthernetEventType.LinkUp); + } + + // ----------------------------------------------------------------------- + // ICrestronConsole — CapturingCrestronConsole + // ----------------------------------------------------------------------- + + [Fact] + public void CapturingConsole_PrintLine_CapturesMessage() + { + var console = new CapturingCrestronConsole(); + + console.PrintLine("hello world"); + + console.Lines.Should().ContainSingle().Which.Should().Be("hello world"); + } + + [Fact] + public void CapturingConsole_AddNewConsoleCommand_RecordsCommandName() + { + var console = new CapturingCrestronConsole(); + + console.AddNewConsoleCommand(_ => { }, "appdebug", "Sets debug level", ConsoleAccessLevel.AccessOperator); + + console.RegisteredCommands.Should().ContainSingle() + .Which.Command.Should().Be("appdebug"); + } +} diff --git a/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj b/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj new file mode 100644 index 00000000..54b74d00 --- /dev/null +++ b/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj @@ -0,0 +1,25 @@ + + + net9.0 + enable + enable + false + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/src/PepperDash.Core/Adapters/CrestronConsoleAdapter.cs b/src/PepperDash.Core/Adapters/CrestronConsoleAdapter.cs new file mode 100644 index 00000000..8c4a6faf --- /dev/null +++ b/src/PepperDash.Core/Adapters/CrestronConsoleAdapter.cs @@ -0,0 +1,35 @@ +using System; +using Crestron.SimplSharp; +using PdCore = PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Adapters; + +/// +/// Production adapter — delegates ICrestronConsole calls to the real Crestron SDK. +/// +public sealed class CrestronConsoleAdapter : PdCore.ICrestronConsole +{ + public void PrintLine(string message) => CrestronConsole.PrintLine(message); + + public void Print(string message) => CrestronConsole.Print(message); + + public void ConsoleCommandResponse(string message) => + CrestronConsole.ConsoleCommandResponse(message); + + public void AddNewConsoleCommand( + Action callback, + string command, + string helpText, + PdCore.ConsoleAccessLevel accessLevel) + { + var crestronLevel = accessLevel switch + { + PdCore.ConsoleAccessLevel.AccessAdministrator => ConsoleAccessLevelEnum.AccessAdministrator, + PdCore.ConsoleAccessLevel.AccessProgrammer => ConsoleAccessLevelEnum.AccessProgrammer, + _ => ConsoleAccessLevelEnum.AccessOperator, + }; + + // Wrap Action in a lambda — Crestron's delegate is not a standard Action. + CrestronConsole.AddNewConsoleCommand(s => callback(s), command, helpText, crestronLevel); + } +} diff --git a/src/PepperDash.Core/Adapters/CrestronDataStoreAdapter.cs b/src/PepperDash.Core/Adapters/CrestronDataStoreAdapter.cs new file mode 100644 index 00000000..effed284 --- /dev/null +++ b/src/PepperDash.Core/Adapters/CrestronDataStoreAdapter.cs @@ -0,0 +1,42 @@ +using Crestron.SimplSharp.CrestronDataStore; +using PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Adapters; + +/// +/// Production adapter — delegates ICrestronDataStore calls to the real Crestron SDK. +/// +public sealed class CrestronDataStoreAdapter : ICrestronDataStore +{ + public void InitStore() => CrestronDataStoreStatic.InitCrestronDataStore(); + + public bool TryGetLocalInt(string key, out int value) + { + var err = CrestronDataStoreStatic.GetLocalIntValue(key, out value); + return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } + + public bool SetLocalInt(string key, int value) + { + var err = CrestronDataStoreStatic.SetLocalIntValue(key, value); + return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } + + public bool SetLocalUint(string key, uint value) + { + var err = CrestronDataStoreStatic.SetLocalUintValue(key, value); + return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } + + public bool TryGetLocalBool(string key, out bool value) + { + var err = CrestronDataStoreStatic.GetLocalBoolValue(key, out value); + return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } + + public bool SetLocalBool(string key, bool value) + { + var err = CrestronDataStoreStatic.SetLocalBoolValue(key, value); + return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } +} diff --git a/src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs b/src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs new file mode 100644 index 00000000..752e9631 --- /dev/null +++ b/src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs @@ -0,0 +1,68 @@ +using System; +using Crestron.SimplSharp; +using PdCore = PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Adapters; + +/// +/// Production adapter — delegates ICrestronEnvironment calls to the real Crestron SDK. +/// +public sealed class CrestronEnvironmentAdapter : PdCore.ICrestronEnvironment +{ + // Subscribe once in constructor and re-raise as our event types. + private event EventHandler? _programStatusChanged; + private event EventHandler? _ethernetEventReceived; + + public CrestronEnvironmentAdapter() + { + CrestronEnvironment.ProgramStatusEventHandler += type => + _programStatusChanged?.Invoke(this, new PdCore.ProgramStatusEventArgs(MapProgramStatus(type))); + + CrestronEnvironment.EthernetEventHandler += args => + _ethernetEventReceived?.Invoke(this, new PdCore.PepperDashEthernetEventArgs( + args.EthernetEventType == eEthernetEventType.LinkDown + ? PdCore.EthernetEventType.LinkDown + : PdCore.EthernetEventType.LinkUp, + (short)args.EthernetAdapter)); + } + + public PdCore.DevicePlatform DevicePlatform => + CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance + ? PdCore.DevicePlatform.Appliance + : PdCore.DevicePlatform.Server; + + public PdCore.RuntimeEnvironment RuntimeEnvironment => + CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro + ? PdCore.RuntimeEnvironment.SimplSharpPro + : PdCore.RuntimeEnvironment.Other; + + public string NewLine => CrestronEnvironment.NewLine; + + public uint ApplicationNumber => InitialParametersClass.ApplicationNumber; + + public uint RoomId => uint.TryParse(InitialParametersClass.RoomId, out var r) ? r : 0; + + public event EventHandler ProgramStatusChanged + { + add => _programStatusChanged += value; + remove => _programStatusChanged -= value; + } + + public event EventHandler EthernetEventReceived + { + add => _ethernetEventReceived += value; + remove => _ethernetEventReceived -= value; + } + + public string GetApplicationRootDirectory() => + Crestron.SimplSharp.CrestronIO.Directory.GetApplicationRootDirectory(); + + private static PdCore.ProgramStatusEventType MapProgramStatus(eProgramStatusEventType type) => + type switch + { + eProgramStatusEventType.Stopping => PdCore.ProgramStatusEventType.Stopping, + eProgramStatusEventType.Paused => PdCore.ProgramStatusEventType.Paused, + eProgramStatusEventType.Resumed => PdCore.ProgramStatusEventType.Resumed, + _ => PdCore.ProgramStatusEventType.Starting, + }; +} diff --git a/src/PepperDash.Core/Adapters/CrestronEthernetAdapter.cs b/src/PepperDash.Core/Adapters/CrestronEthernetAdapter.cs new file mode 100644 index 00000000..f2c80e5b --- /dev/null +++ b/src/PepperDash.Core/Adapters/CrestronEthernetAdapter.cs @@ -0,0 +1,39 @@ +using System; +using Crestron.SimplSharp; +using PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Adapters; + +/// +/// Production adapter — delegates IEthernetHelper calls to the real Crestron SDK. +/// +public sealed class CrestronEthernetAdapter : IEthernetHelper +{ + public string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId) + { + var crestronParam = parameter switch + { + EthernetParameterType.GetCurrentIpAddress => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, + EthernetParameterType.GetHostname => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, + EthernetParameterType.GetDomainName => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, + EthernetParameterType.GetLinkStatus => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_LINK_STATUS, + EthernetParameterType.GetCurrentDhcpState => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, + EthernetParameterType.GetCurrentIpMask => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, + EthernetParameterType.GetCurrentRouter => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, + EthernetParameterType.GetMacAddress => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, + EthernetParameterType.GetDnsServer => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DNS_SERVER, + _ => throw new ArgumentOutOfRangeException(nameof(parameter), parameter, null), + }; + + return CrestronEthernetHelper.GetEthernetParameter(crestronParam, ethernetAdapterId); + } +} diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 2e4b25e4..fb0483bb 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -11,6 +11,7 @@ using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronLogger; using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; +using PdCore = PepperDash.Core.Abstractions; using PepperDash.Core.Logging; using Serilog; using Serilog.Context; @@ -45,6 +46,13 @@ public static class Debug private static ILogger _logger; + // Injected service abstractions. Populated by DebugServiceRegistration.Register() + // before the Debug static constructor runs. Null when running on hardware without + // pre-registration (the static ctor falls back to the Crestron SDK directly). + private static PdCore.ICrestronEnvironment _environment; + private static PdCore.ICrestronConsole _console; + private static PdCore.ICrestronDataStore _dataStore; + private static readonly LoggingLevelSwitch consoleLoggingLevelSwitch; private static readonly LoggingLevelSwitch websocketLoggingLevelSwitch; @@ -107,7 +115,7 @@ public static class Debug /// /// Indicates whether the code is running on an appliance or not. Used to determine file paths and other appliance vs server differences /// - public static bool IsRunningOnAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; + public static bool IsRunningOnAppliance; // set in static constructor /// /// Version for the currently loaded PepperDashCore dll @@ -145,102 +153,114 @@ public static class Debug { try { - CrestronDataStoreStatic.InitCrestronDataStore(); + // Pick up services pre-registered by the composition root (or test setup). + // If null, fall back to direct Crestron SDK calls for production hardware. + _environment = PdCore.DebugServiceRegistration.Environment; + _console = PdCore.DebugServiceRegistration.Console; + _dataStore = PdCore.DebugServiceRegistration.DataStore; + + IsRunningOnAppliance = _environment?.DevicePlatform == PdCore.DevicePlatform.Appliance; + + _dataStore?.InitStore(); consoleDebugTimer = new Timer(defaultConsoleDebugTimeoutMin * 60000) { AutoReset = false }; consoleDebugTimer.Elapsed += (s, e) => { SetDebugLevel(LogEventLevel.Information); - CrestronConsole.ConsoleCommandResponse($"Console debug level reset to {LogEventLevel.Information} after timeout of {defaultConsoleDebugTimeoutMin} minutes"); + _console?.ConsoleCommandResponse($"Console debug level reset to {LogEventLevel.Information} after timeout of {defaultConsoleDebugTimeoutMin} minutes"); }; var defaultConsoleLevel = GetStoredLogEventLevel(LevelStoreKey); - var defaultWebsocketLevel = GetStoredLogEventLevel(WebSocketLevelStoreKey); - var defaultErrorLogLevel = GetStoredLogEventLevel(ErrorLogLevelStoreKey); - var defaultFileLogLevel = GetStoredLogEventLevel(FileLevelStoreKey); consoleLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultConsoleLevel); - websocketLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultWebsocketLevel); - errorLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultErrorLogLevel); - fileLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel); websocketSink = new DebugWebsocketSink(new JsonFormatter(renderMessage: true)); - var logFilePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? - $@"{Directory.GetApplicationRootDirectory()}{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}debug{Path.DirectorySeparatorChar}app{InitialParametersClass.ApplicationNumber}{Path.DirectorySeparatorChar}global-log.log" : - $@"{Directory.GetApplicationRootDirectory()}{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}debug{Path.DirectorySeparatorChar}room{InitialParametersClass.RoomId}{Path.DirectorySeparatorChar}global-log.log"; + var appRoot = _environment?.GetApplicationRootDirectory() + ?? System.IO.Path.GetTempPath(); + var sep = System.IO.Path.DirectorySeparatorChar; + var appNum = _environment?.ApplicationNumber ?? 0; + var roomId = _environment?.RoomId ?? 0; - CrestronConsole.PrintLine($"Saving log files to {logFilePath}"); + var logFilePath = IsRunningOnAppliance + ? $"{appRoot}{sep}user{sep}debug{sep}app{appNum}{sep}global-log.log" + : $"{appRoot}{sep}user{sep}debug{sep}room{roomId}{sep}global-log.log"; - var errorLogTemplate = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance - ? "{@t:fff}ms [{@l:u4}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}" - : "[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}"; + _console?.PrintLine($"Saving log files to {logFilePath}"); + // Build the base Serilog pipeline — sinks that require the Crestron SDK are + // added conditionally so the logger remains usable in test environments. _defaultLoggerConfiguration = new LoggerConfiguration() .MinimumLevel.Verbose() .Enrich.FromLogContext() - .Enrich.With(new CrestronEnricher()) .WriteTo.Sink(new DebugConsoleSink(new ExpressionTemplate("[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}")), levelSwitch: consoleLoggingLevelSwitch) .WriteTo.Sink(websocketSink, levelSwitch: websocketLoggingLevelSwitch) - .WriteTo.Sink(new DebugErrorLogSink(new ExpressionTemplate(errorLogTemplate)), levelSwitch: errorLogLevelSwitch) .WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath, rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Debug, - retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60, + retainedFileCountLimit: IsRunningOnAppliance ? 30 : 60, levelSwitch: fileLoggingLevelSwitch ); - // Instantiate the root logger - _loggerConfiguration = _defaultLoggerConfiguration; - - _logger = _loggerConfiguration.CreateLogger(); - // Get the assembly version and print it to console and the log - GetVersion(); - - string msg = $"[App {InitialParametersClass.ApplicationNumber}] Using PepperDash_Core v{PepperDashCoreVersion}"; - - if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server) + // Add Crestron-specific enricher and error-log sink only when running on hardware. + if (_environment != null) { - msg = $"[Room {InitialParametersClass.RoomId}] Using PepperDash_Core v{PepperDashCoreVersion}"; + var errorLogTemplate = IsRunningOnAppliance + ? "{@t:fff}ms [{@l:u4}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}" + : "[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}"; + + _defaultLoggerConfiguration + .Enrich.With(new CrestronEnricher()) + .WriteTo.Sink(new DebugErrorLogSink(new ExpressionTemplate(errorLogTemplate)), levelSwitch: errorLogLevelSwitch); } - CrestronConsole.PrintLine(msg); + _loggerConfiguration = _defaultLoggerConfiguration; + _logger = _loggerConfiguration.CreateLogger(); + GetVersion(); + + string msg = IsRunningOnAppliance + ? $"[App {appNum}] Using PepperDash_Core v{PepperDashCoreVersion}" + : $"[Room {roomId}] Using PepperDash_Core v{PepperDashCoreVersion}"; + + _console?.PrintLine(msg); LogMessage(LogEventLevel.Information, msg); IncludedExcludedKeys = new Dictionary(); - if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro) + if (_environment?.RuntimeEnvironment == PdCore.RuntimeEnvironment.SimplSharpPro) { - // Add command to console - CrestronConsole.AddNewConsoleCommand(SetDoNotLoadOnNextBootFromConsole, "donotloadonnextboot", - "donotloadonnextboot:P [true/false]: Should the application load on next boot", ConsoleAccessLevelEnum.AccessOperator); + _console?.AddNewConsoleCommand(SetDoNotLoadOnNextBootFromConsole, "donotloadonnextboot", + "donotloadonnextboot:P [true/false]: Should the application load on next boot", + PdCore.ConsoleAccessLevel.AccessOperator); - CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", + _console?.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", "appdebug:P [0-5]: Sets the application's console debug message level", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(ShowDebugLog, "appdebuglog", - "appdebuglog:P [all] Use \"all\" for full log.", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronLogger.Clear(false), "appdebugclear", - "appdebugclear:P Clears the current custom log", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(SetDebugFilterFromConsole, "appdebugfilter", - "appdebugfilter [params]", ConsoleAccessLevelEnum.AccessOperator); - } + PdCore.ConsoleAccessLevel.AccessOperator); - // CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; + _console?.AddNewConsoleCommand(ShowDebugLog, "appdebuglog", + "appdebuglog:P [all] Use \"all\" for full log.", + PdCore.ConsoleAccessLevel.AccessOperator); + + _console?.AddNewConsoleCommand(s => CrestronLogger.Clear(false), "appdebugclear", + "appdebugclear:P Clears the current custom log", + PdCore.ConsoleAccessLevel.AccessOperator); + + _console?.AddNewConsoleCommand(SetDebugFilterFromConsole, "appdebugfilter", + "appdebugfilter [params]", + PdCore.ConsoleAccessLevel.AccessOperator); + } DoNotLoadConfigOnNextBoot = GetDoNotLoadOnNextBoot(); if (DoNotLoadConfigOnNextBoot) - CrestronConsole.PrintLine(string.Format("Program {0} will not load config after next boot. Use console command go:{0} to load the config manually", InitialParametersClass.ApplicationNumber)); + _console?.PrintLine($"Program {appNum} will not load config after next boot. Use console command go:{appNum} to load the config manually"); consoleLoggingLevelSwitch.MinimumLevelChanged += (sender, args) => { @@ -250,18 +270,20 @@ public static class Debug catch (Exception ex) { // _logger may not have been initialized yet — do not call LogError here. - CrestronConsole.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); + // _console may also be null; fall back to CrestronConsole as last resort. + try { _console?.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); } + catch { CrestronConsole.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); } } } private static bool GetDoNotLoadOnNextBoot() { - var err = CrestronDataStoreStatic.GetLocalBoolValue(DoNotLoadOnNextBootKey, out var doNotLoad); + if (_dataStore == null) return false; - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + if (!_dataStore.TryGetLocalBool(DoNotLoadOnNextBootKey, out var doNotLoad)) { - LogError("Error retrieving DoNotLoadOnNextBoot value: {err}", err); - doNotLoad = false; + LogError("Error retrieving DoNotLoadOnNextBoot value"); + return false; } return doNotLoad; @@ -294,17 +316,17 @@ public static class Debug { try { - var result = CrestronDataStoreStatic.GetLocalIntValue(levelStoreKey, out int logLevel); + if (_dataStore == null) return LogEventLevel.Information; - if (result != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + if (!_dataStore.TryGetLocalInt(levelStoreKey, out int logLevel)) { - CrestronConsole.Print($"Unable to retrieve stored log level for {levelStoreKey}.\r\nError: {result}.\r\nSetting level to {LogEventLevel.Information}\r\n"); + _console?.Print($"Unable to retrieve stored log level for {levelStoreKey}. Setting level to {LogEventLevel.Information}\r\n"); return LogEventLevel.Information; } if (logLevel < 0 || logLevel > 5) { - CrestronConsole.PrintLine($"Stored Log level not valid for {levelStoreKey}: {logLevel}. Setting level to {LogEventLevel.Information}"); + _console?.PrintLine($"Stored Log level not valid for {levelStoreKey}: {logLevel}. Setting level to {LogEventLevel.Information}"); return LogEventLevel.Information; } @@ -312,7 +334,7 @@ public static class Debug } catch (Exception ex) { - CrestronConsole.PrintLine($"Exception retrieving log level for {levelStoreKey}: {ex.Message}"); + _console?.PrintLine($"Exception retrieving log level for {levelStoreKey}: {ex.Message}"); return LogEventLevel.Information; } } @@ -369,7 +391,7 @@ public static class Debug { if (levelString.Trim() == "?") { - CrestronConsole.ConsoleCommandResponse( + _console?.ConsoleCommandResponse( "Used to set the minimum level of debug messages to be printed to the console:\r\n" + "[LogLevel] [TimeoutInMinutes]\r\n" + @@ -386,7 +408,7 @@ public static class Debug if (string.IsNullOrEmpty(levelString.Trim())) { - CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", consoleLoggingLevelSwitch.MinimumLevel); + _console?.ConsoleCommandResponse($"AppDebug level = {consoleLoggingLevelSwitch.MinimumLevel}"); return; } @@ -406,7 +428,7 @@ public static class Debug { if (levelInt < 0 || levelInt > 5) { - CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level. If using a number, value must be between 0-5"); + _console?.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level. If using a number, value must be between 0-5"); return; } SetDebugLevel((uint)levelInt); @@ -420,11 +442,11 @@ public static class Debug return; } - CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level"); + _console?.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level"); } catch { - CrestronConsole.ConsoleCommandResponse("Usage: appdebug:P [0-5]"); + _console?.ConsoleCommandResponse("Usage: appdebug:P [0-5]"); } } @@ -439,7 +461,7 @@ public static class Debug { logLevel = LogEventLevel.Information; - CrestronConsole.ConsoleCommandResponse($"{level} not valid. Setting level to {logLevel}"); + _console?.ConsoleCommandResponse($"{level} not valid. Setting level to {logLevel}"); SetDebugLevel(logLevel, timeout); } @@ -460,17 +482,14 @@ public static class Debug consoleLoggingLevelSwitch.MinimumLevel = level; - CrestronConsole.ConsoleCommandResponse("[Application {0}], Debug level set to {1}\r\n", - InitialParametersClass.ApplicationNumber, consoleLoggingLevelSwitch.MinimumLevel); + var appNum = _environment?.ApplicationNumber ?? 0; + _console?.ConsoleCommandResponse($"[Application {appNum}], Debug level set to {consoleLoggingLevelSwitch.MinimumLevel}\r\n"); + _console?.ConsoleCommandResponse($"Storing level {level}:{(int)level}"); - CrestronConsole.ConsoleCommandResponse($"Storing level {level}:{(int)level}"); - - var err = CrestronDataStoreStatic.SetLocalIntValue(LevelStoreKey, (int)level); - - CrestronConsole.ConsoleCommandResponse($"Store result: {err}:{(int)level}"); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - CrestronConsole.PrintLine($"Error saving console debug level setting: {err}"); + if (_dataStore != null && !_dataStore.SetLocalInt(LevelStoreKey, (int)level)) + _console?.PrintLine($"Error saving console debug level setting"); + else + _console?.ConsoleCommandResponse($"Store result: {(int)level}"); } /// @@ -481,10 +500,8 @@ public static class Debug { websocketLoggingLevelSwitch.MinimumLevel = level; - var err = CrestronDataStoreStatic.SetLocalUintValue(WebSocketLevelStoreKey, (uint)level); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting: {error}", err); + if (_dataStore != null && !_dataStore.SetLocalUint(WebSocketLevelStoreKey, (uint)level)) + LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting"); LogMessage(LogEventLevel.Information, "Websocket debug level set to {0}", websocketLoggingLevelSwitch.MinimumLevel); } @@ -498,10 +515,8 @@ public static class Debug { errorLogLevelSwitch.MinimumLevel = level; - var err = CrestronDataStoreStatic.SetLocalUintValue(ErrorLogLevelStoreKey, (uint)level); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogMessage(LogEventLevel.Information, "Error saving Error Log debug level setting: {error}", err); + if (_dataStore != null && !_dataStore.SetLocalUint(ErrorLogLevelStoreKey, (uint)level)) + LogMessage(LogEventLevel.Information, "Error saving Error Log debug level setting"); LogMessage(LogEventLevel.Information, "Error log debug level set to {0}", errorLogLevelSwitch.MinimumLevel); } @@ -513,10 +528,8 @@ public static class Debug { fileLoggingLevelSwitch.MinimumLevel = level; - var err = CrestronDataStoreStatic.SetLocalUintValue(FileLevelStoreKey, (uint)level); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogMessage(LogEventLevel.Information, "Error saving File debug level setting: {error}", err); + if (_dataStore != null && !_dataStore.SetLocalUint(FileLevelStoreKey, (uint)level)) + LogMessage(LogEventLevel.Information, "Error saving File debug level setting"); LogMessage(LogEventLevel.Information, "File debug level set to {0}", fileLoggingLevelSwitch.MinimumLevel); } @@ -531,7 +544,7 @@ public static class Debug { if (string.IsNullOrEmpty(stateString.Trim())) { - CrestronConsole.ConsoleCommandResponse("DoNotLoadOnNextBoot = {0}", DoNotLoadConfigOnNextBoot); + _console?.ConsoleCommandResponse($"DoNotLoadOnNextBoot = {DoNotLoadConfigOnNextBoot}"); return; } @@ -539,7 +552,7 @@ public static class Debug } catch { - CrestronConsole.ConsoleCommandResponse("Usage: donotloadonnextboot:P [true/false]"); + _console?.ConsoleCommandResponse("Usage: donotloadonnextboot:P [true/false]"); } } @@ -655,10 +668,8 @@ public static class Debug { DoNotLoadConfigOnNextBoot = state; - var err = CrestronDataStoreStatic.SetLocalBoolValue(DoNotLoadOnNextBootKey, state); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogError("Error saving console debug level setting: {err}", err); + if (_dataStore != null && !_dataStore.SetLocalBool(DoNotLoadOnNextBootKey, state)) + LogError("Error saving DoNotLoadConfigOnNextBoot setting"); LogInformation("Do Not Load Config on Next Boot set to {state}", DoNotLoadConfigOnNextBoot); } @@ -668,9 +679,10 @@ public static class Debug /// public static void ShowDebugLog(string s) { + if (_environment == null) return; // CrestronLogger not available in test environments var loglist = CrestronLogger.PrintTheLog(s.ToLower() == "all"); foreach (var l in loglist) - CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine); + _console?.ConsoleCommandResponse(l + CrestronEnvironment.NewLine); } /// diff --git a/src/PepperDash.Core/PepperDash.Core.csproj b/src/PepperDash.Core/PepperDash.Core.csproj index 6ff5dd71..4cd091ef 100644 --- a/src/PepperDash.Core/PepperDash.Core.csproj +++ b/src/PepperDash.Core/PepperDash.Core.csproj @@ -50,6 +50,9 @@ + + + diff --git a/src/PepperDash.Essentials.Core.Tests/Config/ConfigServiceFakesTests.cs b/src/PepperDash.Essentials.Core.Tests/Config/ConfigServiceFakesTests.cs new file mode 100644 index 00000000..1359a450 --- /dev/null +++ b/src/PepperDash.Essentials.Core.Tests/Config/ConfigServiceFakesTests.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using PepperDash.Core.Abstractions; +using PepperDash.Core.Tests.Fakes; +using Xunit; + +namespace PepperDash.Essentials.Core.Tests.Config; + +/// +/// Tests for the configuration loading abstractions. +/// These verify behaviour of the test fakes and interfaces independently of +/// any Crestron SDK types (ConfigReader itself will be tested here once it +/// is migrated from Crestron.SimplSharp.CrestronIO to System.IO — see plan Phase 4). +/// +public class ConfigServiceFakesTests +{ + [Fact] + public void DataStore_MultipleKeys_AreStoredIndependently() + { + var store = new InMemoryCrestronDataStore(); + store.InitStore(); + + store.SetLocalInt("KeyA", 1); + store.SetLocalInt("KeyB", 2); + + store.TryGetLocalInt("KeyA", out var a); + store.TryGetLocalInt("KeyB", out var b); + + a.Should().Be(1); + b.Should().Be(2); + } + + [Fact] + public void DataStore_OverwriteKey_ReturnsNewValue() + { + var store = new InMemoryCrestronDataStore(); + store.SetLocalInt("Level", 1); + store.SetLocalInt("Level", 5); + + store.TryGetLocalInt("Level", out var level); + level.Should().Be(5); + } + + [Fact] + public void FakeEnvironment_CanBeConfiguredForServer() + { + var env = new FakeCrestronEnvironment + { + DevicePlatform = DevicePlatform.Server, + ApplicationNumber = 1, + RoomId = 42, + }; + + env.DevicePlatform.Should().Be(DevicePlatform.Server); + env.RoomId.Should().Be(42u); + } + + [Fact] + public void FakeEthernetHelper_SeedAndRetrieve() + { + var eth = new FakeEthernetHelper() + .Seed(EthernetParameterType.GetCurrentIpAddress, "192.168.1.100") + .Seed(EthernetParameterType.GetHostname, "MC4-TEST"); + + eth.GetEthernetParameter(EthernetParameterType.GetCurrentIpAddress, 0) + .Should().Be("192.168.1.100"); + + eth.GetEthernetParameter(EthernetParameterType.GetHostname, 0) + .Should().Be("MC4-TEST"); + + eth.GetEthernetParameter(EthernetParameterType.GetDomainName, 0) + .Should().BeEmpty("unseeded parameter should return empty string"); + } +} diff --git a/src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj b/src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj new file mode 100644 index 00000000..429afd56 --- /dev/null +++ b/src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj @@ -0,0 +1,27 @@ + + + net9.0 + enable + enable + false + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 63f4160e..ffb8a2d3 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -7,6 +7,8 @@ using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.Diagnostics; using PepperDash.Core; +using PepperDash.Core.Abstractions; +using PepperDash.Core.Adapters; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; @@ -46,6 +48,14 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig { try { + // Register Crestron service adapters BEFORE the first reference to Debug, + // so that Debug's static constructor uses these implementations instead of + // calling the Crestron SDK statics directly. + DebugServiceRegistration.Register( + new CrestronEnvironmentAdapter(), + new CrestronConsoleAdapter(), + new CrestronDataStoreAdapter()); + Crestron.SimplSharpPro.CrestronThread.Thread.MaxNumberOfUserThreads = 400; Global.ControlSystem = this;