mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-04-20 07:56:50 +00:00
feat: Enhance device testing and environment handling with new interfaces and fakes
This commit is contained in:
parent
24df4e7a03
commit
37961b9eac
11 changed files with 421 additions and 33 deletions
|
|
@ -29,6 +29,13 @@ public interface ICrestronEnvironment
|
||||||
|
|
||||||
/// <summary>Gets the application root directory path.</summary>
|
/// <summary>Gets the application root directory path.</summary>
|
||||||
string GetApplicationRootDirectory();
|
string GetApplicationRootDirectory();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns <c>true</c> when running on real Crestron hardware.
|
||||||
|
/// Returns <c>false</c> in test / dev environments so that SDK-specific
|
||||||
|
/// sinks and enrichers can be safely skipped.
|
||||||
|
/// </summary>
|
||||||
|
bool IsHardwareRuntime { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
323
src/PepperDash.Core.Tests/Devices/DeviceTests.cs
Normal file
323
src/PepperDash.Core.Tests/Devices/DeviceTests.cs
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
using FluentAssertions;
|
||||||
|
using PepperDash.Core;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PepperDash.Core.Tests.Devices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for <see cref="Device"/> — the base class for all PepperDash devices.
|
||||||
|
/// These run without Crestron hardware; Debug is initialized with fakes via TestInitializer.
|
||||||
|
/// </summary>
|
||||||
|
public class DeviceTests
|
||||||
|
{
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Construction
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_SingleArg_SetsKey()
|
||||||
|
{
|
||||||
|
var device = new Device("my-device");
|
||||||
|
|
||||||
|
device.Key.Should().Be("my-device");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_SingleArg_SetsNameToEmpty()
|
||||||
|
{
|
||||||
|
var device = new Device("my-device");
|
||||||
|
|
||||||
|
device.Name.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_TwoArg_SetsKeyAndName()
|
||||||
|
{
|
||||||
|
var device = new Device("my-device", "My Device");
|
||||||
|
|
||||||
|
device.Key.Should().Be("my-device");
|
||||||
|
device.Name.Should().Be("My Device");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_KeyWithDot_StillSetsKey()
|
||||||
|
{
|
||||||
|
// The dot triggers a debug log warning but must not prevent construction.
|
||||||
|
var device = new Device("parent.child");
|
||||||
|
|
||||||
|
device.Key.Should().Be("parent.child");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// ToString
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToString_WithName_FormatsKeyDashName()
|
||||||
|
{
|
||||||
|
var device = new Device("cam-01", "Front Camera");
|
||||||
|
|
||||||
|
device.ToString().Should().Be("cam-01 - Front Camera");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToString_WithoutName_UsesDashPlaceholder()
|
||||||
|
{
|
||||||
|
var device = new Device("cam-01");
|
||||||
|
|
||||||
|
device.ToString().Should().Be("cam-01 - ---");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// DefaultDevice
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultDevice_IsNotNull()
|
||||||
|
{
|
||||||
|
Device.DefaultDevice.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultDevice_HasKeyDefault()
|
||||||
|
{
|
||||||
|
Device.DefaultDevice.Key.Should().Be("Default");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// CustomActivate / Activate / Deactivate / Initialize
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CustomActivate_DefaultReturnTrue()
|
||||||
|
{
|
||||||
|
var device = new TestDevice("d1");
|
||||||
|
|
||||||
|
device.CallCustomActivate().Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Deactivate_DefaultReturnsTrue()
|
||||||
|
{
|
||||||
|
var device = new Device("d1");
|
||||||
|
|
||||||
|
device.Deactivate().Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Activate_CallsCustomActivate_AndReturnsItsResult()
|
||||||
|
{
|
||||||
|
var stub = new ActivateTrackingDevice("d1", result: false);
|
||||||
|
|
||||||
|
stub.Activate().Should().BeFalse();
|
||||||
|
stub.CustomActivateCalled.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Activate_TrueWhenCustomActivateReturnsTrue()
|
||||||
|
{
|
||||||
|
var stub = new ActivateTrackingDevice("d1", result: true);
|
||||||
|
|
||||||
|
stub.Activate().Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Initialize_DoesNotThrow()
|
||||||
|
{
|
||||||
|
var device = new TestDevice("d1");
|
||||||
|
var act = () => device.CallInitialize();
|
||||||
|
|
||||||
|
act.Should().NotThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// PreActivate
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreActivate_NoActions_DoesNotThrow()
|
||||||
|
{
|
||||||
|
var device = new TestDevice("d1");
|
||||||
|
var act = () => device.PreActivate();
|
||||||
|
|
||||||
|
act.Should().NotThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreActivate_RunsRegisteredActionsInOrder()
|
||||||
|
{
|
||||||
|
var device = new TestDevice("d1");
|
||||||
|
var order = new List<int>();
|
||||||
|
|
||||||
|
device.AddPreActivationAction(() => order.Add(1));
|
||||||
|
device.AddPreActivationAction(() => order.Add(2));
|
||||||
|
device.AddPreActivationAction(() => order.Add(3));
|
||||||
|
|
||||||
|
device.PreActivate();
|
||||||
|
|
||||||
|
order.Should().Equal(1, 2, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreActivate_ContinuesAfterFaultingAction()
|
||||||
|
{
|
||||||
|
var device = new TestDevice("d1");
|
||||||
|
var reached = false;
|
||||||
|
|
||||||
|
device.AddPreActivationAction(() => throw new InvalidOperationException("boom"));
|
||||||
|
device.AddPreActivationAction(() => reached = true);
|
||||||
|
|
||||||
|
var act = () => device.PreActivate();
|
||||||
|
|
||||||
|
act.Should().NotThrow("exceptions in individual actions must be caught internally");
|
||||||
|
reached.Should().BeTrue("actions after a faulting action must still run");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// PostActivate
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PostActivate_NoActions_DoesNotThrow()
|
||||||
|
{
|
||||||
|
var device = new TestDevice("d1");
|
||||||
|
var act = () => device.PostActivate();
|
||||||
|
|
||||||
|
act.Should().NotThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PostActivate_RunsRegisteredActionsInOrder()
|
||||||
|
{
|
||||||
|
var device = new TestDevice("d1");
|
||||||
|
var order = new List<int>();
|
||||||
|
|
||||||
|
device.AddPostActivationAction(() => order.Add(1));
|
||||||
|
device.AddPostActivationAction(() => order.Add(2));
|
||||||
|
|
||||||
|
device.PostActivate();
|
||||||
|
|
||||||
|
order.Should().Equal(1, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PostActivate_ContinuesAfterFaultingAction()
|
||||||
|
{
|
||||||
|
var device = new TestDevice("d1");
|
||||||
|
var reached = false;
|
||||||
|
|
||||||
|
device.AddPostActivationAction(() => throw new Exception("boom"));
|
||||||
|
device.AddPostActivationAction(() => reached = true);
|
||||||
|
|
||||||
|
var act = () => device.PostActivate();
|
||||||
|
|
||||||
|
act.Should().NotThrow();
|
||||||
|
reached.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Pre and Post actions are independent lists
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreActivationActions_DoNotRunOnPostActivate()
|
||||||
|
{
|
||||||
|
var device = new TestDevice("d1");
|
||||||
|
var preRan = false;
|
||||||
|
|
||||||
|
device.AddPreActivationAction(() => preRan = true);
|
||||||
|
device.PostActivate();
|
||||||
|
|
||||||
|
preRan.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PostActivationActions_DoNotRunOnPreActivate()
|
||||||
|
{
|
||||||
|
var device = new TestDevice("d1");
|
||||||
|
var postRan = false;
|
||||||
|
|
||||||
|
device.AddPostActivationAction(() => postRan = true);
|
||||||
|
device.PreActivate();
|
||||||
|
|
||||||
|
postRan.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// OnFalse
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnFalse_FiresAction_WhenBoolIsFalse()
|
||||||
|
{
|
||||||
|
var device = new Device("d1");
|
||||||
|
var fired = false;
|
||||||
|
|
||||||
|
device.OnFalse(false, () => fired = true);
|
||||||
|
|
||||||
|
fired.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnFalse_DoesNotFireAction_WhenBoolIsTrue()
|
||||||
|
{
|
||||||
|
var device = new Device("d1");
|
||||||
|
var fired = false;
|
||||||
|
|
||||||
|
device.OnFalse(true, () => fired = true);
|
||||||
|
|
||||||
|
fired.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnFalse_DoesNotFireAction_ForNonBoolType()
|
||||||
|
{
|
||||||
|
var device = new Device("d1");
|
||||||
|
var fired = false;
|
||||||
|
|
||||||
|
device.OnFalse("not a bool", () => fired = true);
|
||||||
|
device.OnFalse(0, () => fired = true);
|
||||||
|
device.OnFalse(null!, () => fired = true);
|
||||||
|
|
||||||
|
fired.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exposes protected Device members so test methods can call them directly.
|
||||||
|
/// </summary>
|
||||||
|
private class TestDevice : Device
|
||||||
|
{
|
||||||
|
public TestDevice(string key) : base(key) { }
|
||||||
|
public TestDevice(string key, string name) : base(key, name) { }
|
||||||
|
|
||||||
|
public void AddPreActivationAction(Action act) => base.AddPreActivationAction(act);
|
||||||
|
public void AddPostActivationAction(Action act) => base.AddPostActivationAction(act);
|
||||||
|
public bool CallCustomActivate() => base.CustomActivate();
|
||||||
|
public void CallInitialize() => base.Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records whether CustomActivate was invoked and returns a configured result.
|
||||||
|
/// Used to verify Activate() correctly delegates to CustomActivate().
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ActivateTrackingDevice : Device
|
||||||
|
{
|
||||||
|
private readonly bool _result;
|
||||||
|
public bool CustomActivateCalled { get; private set; }
|
||||||
|
|
||||||
|
public ActivateTrackingDevice(string key, bool result = true) : base(key)
|
||||||
|
{
|
||||||
|
_result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CustomActivate()
|
||||||
|
{
|
||||||
|
CustomActivateCalled = true;
|
||||||
|
return _result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,9 @@ public class FakeCrestronEnvironment : ICrestronEnvironment
|
||||||
|
|
||||||
public string GetApplicationRootDirectory() => System.IO.Path.GetTempPath();
|
public string GetApplicationRootDirectory() => System.IO.Path.GetTempPath();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsHardwareRuntime => false;
|
||||||
|
|
||||||
/// <summary>Simulates a program status event for tests.</summary>
|
/// <summary>Simulates a program status event for tests.</summary>
|
||||||
public void RaiseProgramStatus(ProgramStatusEventType type) =>
|
public void RaiseProgramStatus(ProgramStatusEventType type) =>
|
||||||
ProgramStatusChanged?.Invoke(this, new ProgramStatusEventArgs(type));
|
ProgramStatusChanged?.Invoke(this, new ProgramStatusEventArgs(type));
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,9 @@
|
||||||
<!-- Reference the Abstractions project only — no Crestron SDK dependency -->
|
<!-- Reference the Abstractions project only — no Crestron SDK dependency -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\PepperDash.Core.Abstractions\PepperDash.Core.Abstractions.csproj" />
|
<ProjectReference Include="..\PepperDash.Core.Abstractions\PepperDash.Core.Abstractions.csproj" />
|
||||||
|
<!-- PepperDash.Core is referenced so we can test Device and other concrete types.
|
||||||
|
DebugServiceRegistration.Register() is called via a [ModuleInitializer] before any
|
||||||
|
test runs, so Debug's static constructor uses fakes and never touches the Crestron SDK. -->
|
||||||
|
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
29
src/PepperDash.Core.Tests/TestInitializer.cs
Normal file
29
src/PepperDash.Core.Tests/TestInitializer.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using PepperDash.Core.Abstractions;
|
||||||
|
using PepperDash.Core.Tests.Fakes;
|
||||||
|
|
||||||
|
namespace PepperDash.Core.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs once before any type in this assembly is accessed.
|
||||||
|
/// Registers fake Crestron service implementations with <see cref="DebugServiceRegistration"/>
|
||||||
|
/// so that the <c>Debug</c> static constructor uses them instead of the real Crestron SDK.
|
||||||
|
/// This must remain a module initializer (not a test fixture) because the static constructor
|
||||||
|
/// fires the first time <em>any</em> type in PepperDash.Core is referenced — before xUnit
|
||||||
|
/// has a chance to run fixture setup code.
|
||||||
|
/// </summary>
|
||||||
|
internal static class TestInitializer
|
||||||
|
{
|
||||||
|
[ModuleInitializer]
|
||||||
|
internal static void Initialize()
|
||||||
|
{
|
||||||
|
DebugServiceRegistration.Register(
|
||||||
|
new FakeCrestronEnvironment
|
||||||
|
{
|
||||||
|
DevicePlatform = DevicePlatform.Server, // avoids any appliance-only code paths
|
||||||
|
RuntimeEnvironment = RuntimeEnvironment.Other, // skips console command registration
|
||||||
|
},
|
||||||
|
new NoOpCrestronConsole(),
|
||||||
|
new InMemoryCrestronDataStore());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,9 @@ public sealed class CrestronEnvironmentAdapter : PdCore.ICrestronEnvironment
|
||||||
public string GetApplicationRootDirectory() =>
|
public string GetApplicationRootDirectory() =>
|
||||||
Crestron.SimplSharp.CrestronIO.Directory.GetApplicationRootDirectory();
|
Crestron.SimplSharp.CrestronIO.Directory.GetApplicationRootDirectory();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsHardwareRuntime => true;
|
||||||
|
|
||||||
private static PdCore.ProgramStatusEventType MapProgramStatus(eProgramStatusEventType type) =>
|
private static PdCore.ProgramStatusEventType MapProgramStatus(eProgramStatusEventType type) =>
|
||||||
type switch
|
type switch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ public class Device : IKeyName
|
||||||
/// Adds a pre activation action
|
/// Adds a pre activation action
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="act"></param>
|
/// <param name="act"></param>
|
||||||
public void AddPreActivationAction(Action act)
|
protected void AddPreActivationAction(Action act)
|
||||||
{
|
{
|
||||||
if (_PreActivationActions == null)
|
if (_PreActivationActions == null)
|
||||||
_PreActivationActions = new List<Action>();
|
_PreActivationActions = new List<Action>();
|
||||||
|
|
@ -89,7 +89,7 @@ public class Device : IKeyName
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AddPostActivationAction method
|
/// AddPostActivationAction method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void AddPostActivationAction(Action act)
|
protected void AddPostActivationAction(Action act)
|
||||||
{
|
{
|
||||||
if (_PostActivationActions == null)
|
if (_PostActivationActions == null)
|
||||||
_PostActivationActions = new List<Action>();
|
_PostActivationActions = new List<Action>();
|
||||||
|
|
@ -156,7 +156,7 @@ public class Device : IKeyName
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CustomActivate method
|
/// CustomActivate method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual bool CustomActivate() { return true; }
|
protected virtual bool CustomActivate() { return true; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Call to deactivate device - unlink events, etc. Overriding classes do not
|
/// Call to deactivate device - unlink events, etc. Overriding classes do not
|
||||||
|
|
@ -168,7 +168,7 @@ public class Device : IKeyName
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
|
/// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Initialize()
|
protected virtual void Initialize()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ public static class Debug
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the file containing the current debug settings.
|
/// The name of the file containing the current debug settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string FileName = string.Format(@"app{0}Debug.json", InitialParametersClass.ApplicationNumber);
|
public static string FileName = "app0Debug.json"; // default; updated in static ctor using _environment.ApplicationNumber
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Debug level to set for a given program.
|
/// Debug level to set for a given program.
|
||||||
|
|
@ -161,6 +161,9 @@ public static class Debug
|
||||||
|
|
||||||
IsRunningOnAppliance = _environment?.DevicePlatform == PdCore.DevicePlatform.Appliance;
|
IsRunningOnAppliance = _environment?.DevicePlatform == PdCore.DevicePlatform.Appliance;
|
||||||
|
|
||||||
|
// Update FileName now that _environment is available (avoids Crestron SDK ref in field initializer).
|
||||||
|
FileName = $"app{_environment?.ApplicationNumber ?? 0}Debug.json";
|
||||||
|
|
||||||
_dataStore?.InitStore();
|
_dataStore?.InitStore();
|
||||||
|
|
||||||
consoleDebugTimer = new Timer(defaultConsoleDebugTimeoutMin * 60000) { AutoReset = false };
|
consoleDebugTimer = new Timer(defaultConsoleDebugTimeoutMin * 60000) { AutoReset = false };
|
||||||
|
|
@ -180,7 +183,7 @@ public static class Debug
|
||||||
errorLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultErrorLogLevel);
|
errorLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultErrorLogLevel);
|
||||||
fileLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel);
|
fileLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel);
|
||||||
|
|
||||||
websocketSink = new DebugWebsocketSink(new JsonFormatter(renderMessage: true));
|
websocketSink = TryCreateWebsocketSink();
|
||||||
|
|
||||||
var appRoot = _environment?.GetApplicationRootDirectory()
|
var appRoot = _environment?.GetApplicationRootDirectory()
|
||||||
?? System.IO.Path.GetTempPath();
|
?? System.IO.Path.GetTempPath();
|
||||||
|
|
@ -200,7 +203,6 @@ public static class Debug
|
||||||
.MinimumLevel.Verbose()
|
.MinimumLevel.Verbose()
|
||||||
.Enrich.FromLogContext()
|
.Enrich.FromLogContext()
|
||||||
.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(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.File(new RenderedCompactJsonFormatter(), logFilePath,
|
.WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath,
|
||||||
rollingInterval: RollingInterval.Day,
|
rollingInterval: RollingInterval.Day,
|
||||||
restrictedToMinimumLevel: LogEventLevel.Debug,
|
restrictedToMinimumLevel: LogEventLevel.Debug,
|
||||||
|
|
@ -208,8 +210,12 @@ public static class Debug
|
||||||
levelSwitch: fileLoggingLevelSwitch
|
levelSwitch: fileLoggingLevelSwitch
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add Crestron-specific enricher and error-log sink only when running on hardware.
|
// Websocket sink is null when DebugWebsocketSink failed to construct (e.g. test env).
|
||||||
if (_environment != null)
|
if (websocketSink != null)
|
||||||
|
_defaultLoggerConfiguration.WriteTo.Sink(websocketSink, levelSwitch: websocketLoggingLevelSwitch);
|
||||||
|
|
||||||
|
// Add Crestron-specific enricher and error-log sink only on real hardware.
|
||||||
|
if (_environment?.IsHardwareRuntime == true)
|
||||||
{
|
{
|
||||||
var errorLogTemplate = IsRunningOnAppliance
|
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:fff}ms [{@l:u4}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}"
|
||||||
|
|
@ -271,8 +277,34 @@ public static class Debug
|
||||||
{
|
{
|
||||||
// _logger may not have been initialized yet — do not call LogError here.
|
// _logger may not have been initialized yet — do not call LogError here.
|
||||||
// _console may also be null; fall back to CrestronConsole as last resort.
|
// _console may also be null; fall back to CrestronConsole as last resort.
|
||||||
|
// IMPORTANT: this catch block must not throw — any exception escaping a static
|
||||||
|
// constructor permanently faults the type, making the entire class unusable.
|
||||||
try { _console?.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); }
|
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}"); }
|
catch
|
||||||
|
{
|
||||||
|
try { CrestronConsole.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); }
|
||||||
|
catch { /* CrestronConsole unavailable (test/dev env) — swallow to keep type initializer healthy */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Guarantee _logger is never null — all Debug.Log* calls are safe even if the
|
||||||
|
// ctor failed partway through (e.g. on a dev machine without Crestron hardware).
|
||||||
|
_logger ??= new LoggerConfiguration().MinimumLevel.Fatal().CreateLogger();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Creates the WebSocket sink, returning null if construction fails in a test/dev environment.</summary>
|
||||||
|
private static DebugWebsocketSink? TryCreateWebsocketSink()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new DebugWebsocketSink(new JsonFormatter(renderMessage: true));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_console?.PrintLine("DebugWebsocketSink could not be created in this environment; websocket logging disabled.");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,13 +91,18 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
|
||||||
if (!File.Exists(CertPath))
|
if (!File.Exists(CertPath))
|
||||||
CreateCert();
|
CreateCert();
|
||||||
|
|
||||||
CrestronEnvironment.ProgramStatusEventHandler += type =>
|
try
|
||||||
{
|
{
|
||||||
if (type == eProgramStatusEventType.Stopping)
|
CrestronEnvironment.ProgramStatusEventHandler += type =>
|
||||||
{
|
{
|
||||||
StopServer();
|
if (type == eProgramStatusEventType.Stopping)
|
||||||
}
|
StopServer();
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// CrestronEnvironment is not available in test / dev environments — safe to skip.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CreateCert()
|
private static void CreateCert()
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,6 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DeviceStateMessageBase : DeviceMessageBase
|
public class DeviceStateMessageBase : DeviceMessageBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The interfaces implmented by the device sending the messsage
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("interfaces")]
|
|
||||||
public List<string> Interfaces { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the interfaces implemented by the device sending the message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="interfaces"></param>
|
|
||||||
public void SetInterfaces(List<string> interfaces)
|
|
||||||
{
|
|
||||||
Interfaces = interfaces;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -258,8 +258,6 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
throw new ArgumentNullException("device");
|
throw new ArgumentNullException("device");
|
||||||
}
|
}
|
||||||
|
|
||||||
message.SetInterfaces(_deviceInterfaces);
|
|
||||||
|
|
||||||
message.Key = _device.Key;
|
message.Key = _device.Key;
|
||||||
|
|
||||||
message.Name = _device.Name;
|
message.Name = _device.Name;
|
||||||
|
|
@ -285,8 +283,6 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
deviceState.SetInterfaces(_deviceInterfaces);
|
|
||||||
|
|
||||||
deviceState.Key = _device.Key;
|
deviceState.Key = _device.Key;
|
||||||
|
|
||||||
deviceState.Name = _device.Name;
|
deviceState.Name = _device.Name;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue