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.
This commit is contained in:
Neil Dorin 2026-04-08 12:41:15 -06:00
parent bfb9838743
commit 24df4e7a03
21 changed files with 1020 additions and 93 deletions

View file

@ -0,0 +1,38 @@
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Tests.Fakes;
/// <summary>
/// No-op ICrestronConsole that captures output for test assertions.
/// </summary>
public class CapturingCrestronConsole : ICrestronConsole
{
public List<string> Lines { get; } = new();
public List<string> 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<string> callback,
string command,
string helpText,
ConsoleAccessLevel accessLevel)
{
RegisteredCommands.Add((command, helpText));
}
}
/// <summary>
/// Minimal no-op ICrestronConsole that discards all output. Useful when you only
/// care about the system under test and not what it logs.
/// </summary>
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 __, string ___, ConsoleAccessLevel ____) { }
}

View file

@ -0,0 +1,46 @@
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Tests.Fakes;
/// <summary>
/// Configurable ICrestronEnvironment for unit tests.
/// Defaults: Appliance / SimplSharpPro / ApplicationNumber=1.
/// </summary>
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<ProgramStatusEventArgs>? ProgramStatusChanged;
public event EventHandler<PepperDashEthernetEventArgs>? EthernetEventReceived;
public string GetApplicationRootDirectory() => System.IO.Path.GetTempPath();
/// <summary>Simulates a program status event for tests.</summary>
public void RaiseProgramStatus(ProgramStatusEventType type) =>
ProgramStatusChanged?.Invoke(this, new ProgramStatusEventArgs(type));
/// <summary>Simulates an ethernet event for tests.</summary>
public void RaiseEthernetEvent(EthernetEventType type, short adapter = 0) =>
EthernetEventReceived?.Invoke(this, new PepperDashEthernetEventArgs(type, adapter));
}
/// <summary>
/// No-op IEthernetHelper that returns configurable values.
/// </summary>
public class FakeEthernetHelper : IEthernetHelper
{
private readonly Dictionary<EthernetParameterType, string> _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;
}

View file

@ -0,0 +1,59 @@
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Tests.Fakes;
/// <summary>
/// In-memory ICrestronDataStore backed by a dictionary.
/// Use in unit tests to verify that keys are read from and written to the store correctly.
/// </summary>
public class InMemoryCrestronDataStore : ICrestronDataStore
{
private readonly Dictionary<string, object> _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;
}
/// <summary>Seeds a key for testing read paths.</summary>
public void Seed(string key, object value) => _store[key] = value;
}

View file

@ -0,0 +1,171 @@
using FluentAssertions;
using PepperDash.Core.Abstractions;
using PepperDash.Core.Tests.Fakes;
using Xunit;
namespace PepperDash.Core.Tests.Logging;
/// <summary>
/// Tests for Debug-related service interfaces and implementations.
/// These tests verify the behaviour of the abstractions in isolation (no Crestron SDK required).
/// </summary>
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");
}
}

View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="FluentAssertions" Version="7.0.0" />
</ItemGroup>
<!-- Reference the Abstractions project only — no Crestron SDK dependency -->
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core.Abstractions\PepperDash.Core.Abstractions.csproj" />
</ItemGroup>
</Project>