diff --git a/src/PepperDash.Essentials.Core/Devices/CrestronProcessorTestable.cs b/src/PepperDash.Essentials.Core/Devices/CrestronProcessorTestable.cs index 60f4597f..e0329589 100644 --- a/src/PepperDash.Essentials.Core/Devices/CrestronProcessorTestable.cs +++ b/src/PepperDash.Essentials.Core/Devices/CrestronProcessorTestable.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using PepperDash.Core; using PepperDash.Essentials.Core.Abstractions; using PepperDash.Essentials.Core.CrestronIO; +using PepperDash.Essentials.Core.Factory; using Serilog.Events; namespace PepperDash.Essentials.Core.Devices @@ -16,11 +17,12 @@ namespace PepperDash.Essentials.Core.Devices public ICrestronControlSystem Processor { get; private set; } - public CrestronProcessorTestable(string key, ICrestronControlSystem processor) + public CrestronProcessorTestable(string key, ICrestronControlSystem processor = null) : base(key) { SwitchedOutputs = new Dictionary(); - Processor = processor ?? throw new ArgumentNullException(nameof(processor)); + // Use factory if processor not provided + Processor = processor ?? CrestronEnvironmentFactory.GetControlSystem(); GetRelays(); } diff --git a/src/PepperDash.Essentials.Core/Factory/CrestronEnvironmentFactory.cs b/src/PepperDash.Essentials.Core/Factory/CrestronEnvironmentFactory.cs new file mode 100644 index 00000000..3f910975 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Factory/CrestronEnvironmentFactory.cs @@ -0,0 +1,136 @@ +using System; +using PepperDash.Essentials.Core.Abstractions; + +namespace PepperDash.Essentials.Core.Factory +{ + /// + /// Factory for creating Crestron environment dependencies + /// Allows switching between real Crestron libraries and mock implementations + /// + public static class CrestronEnvironmentFactory + { + private static ICrestronEnvironmentProvider _provider; + private static bool _isTestMode = false; + + static CrestronEnvironmentFactory() + { + // Default to runtime provider + _provider = new CrestronRuntimeProvider(); + } + + /// + /// Enables test mode with mock implementations + /// + public static void EnableTestMode() + { + _isTestMode = true; + _provider = new CrestronMockProvider(); + } + + /// + /// Disables test mode and returns to runtime implementations + /// + public static void DisableTestMode() + { + _isTestMode = false; + _provider = new CrestronRuntimeProvider(); + } + + /// + /// Sets a custom provider for Crestron environment + /// + public static void SetProvider(ICrestronEnvironmentProvider provider) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + /// + /// Gets whether the factory is in test mode + /// + public static bool IsTestMode => _isTestMode; + + /// + /// Gets the current control system instance + /// + public static ICrestronControlSystem GetControlSystem() + { + return _provider.GetControlSystem(); + } + + /// + /// Creates a relay port + /// + public static IRelayPort CreateRelayPort(uint portNumber) + { + return _provider.CreateRelayPort(portNumber); + } + + /// + /// Creates a digital input + /// + public static IDigitalInput CreateDigitalInput(uint portNumber) + { + return _provider.CreateDigitalInput(portNumber); + } + + /// + /// Creates a versiport + /// + public static IVersiPort CreateVersiPort(uint portNumber) + { + return _provider.CreateVersiPort(portNumber); + } + + /// + /// Gets console manager for debugging + /// + public static IConsoleManager GetConsoleManager() + { + return _provider.GetConsoleManager(); + } + + /// + /// Gets system information + /// + public static ISystemInfo GetSystemInfo() + { + return _provider.GetSystemInfo(); + } + } + + /// + /// Provider interface for Crestron environment dependencies + /// + public interface ICrestronEnvironmentProvider + { + ICrestronControlSystem GetControlSystem(); + IRelayPort CreateRelayPort(uint portNumber); + IDigitalInput CreateDigitalInput(uint portNumber); + IVersiPort CreateVersiPort(uint portNumber); + IConsoleManager GetConsoleManager(); + ISystemInfo GetSystemInfo(); + } + + /// + /// Console manager abstraction + /// + public interface IConsoleManager + { + void Print(string message); + void PrintLine(string message); + void RegisterCommand(string command, Action handler, string help); + } + + /// + /// System information abstraction + /// + public interface ISystemInfo + { + string ProgramName { get; } + string SerialNumber { get; } + string MacAddress { get; } + string IpAddress { get; } + string FirmwareVersion { get; } + DateTime SystemUpTime { get; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Factory/CrestronMockProvider.cs b/src/PepperDash.Essentials.Core/Factory/CrestronMockProvider.cs new file mode 100644 index 00000000..569c0da3 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Factory/CrestronMockProvider.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using PepperDash.Essentials.Core.Abstractions; + +namespace PepperDash.Essentials.Core.Factory +{ + /// + /// Mock provider for unit testing without Crestron hardware + /// + public class CrestronMockProvider : ICrestronEnvironmentProvider + { + private MockControlSystem _controlSystem; + + public CrestronMockProvider() + { + _controlSystem = new MockControlSystem(); + } + + public ICrestronControlSystem GetControlSystem() + { + return _controlSystem; + } + + public IRelayPort CreateRelayPort(uint portNumber) + { + if (!_controlSystem.RelayPorts.ContainsKey(portNumber)) + { + _controlSystem.RelayPorts[portNumber] = new MockRelayPort(); + } + return _controlSystem.RelayPorts[portNumber]; + } + + public IDigitalInput CreateDigitalInput(uint portNumber) + { + if (!_controlSystem.DigitalInputs.ContainsKey(portNumber)) + { + _controlSystem.DigitalInputs[portNumber] = new MockDigitalInput(); + } + return _controlSystem.DigitalInputs[portNumber]; + } + + public IVersiPort CreateVersiPort(uint portNumber) + { + if (!_controlSystem.VersiPorts.ContainsKey(portNumber)) + { + _controlSystem.VersiPorts[portNumber] = new MockVersiPort(); + } + return _controlSystem.VersiPorts[portNumber]; + } + + public IConsoleManager GetConsoleManager() + { + return new MockConsoleManager(); + } + + public ISystemInfo GetSystemInfo() + { + return new MockSystemInfo(); + } + + /// + /// Configures the mock control system for testing + /// + public void ConfigureMockSystem(Action configure) + { + configure(_controlSystem); + } + } + + /// + /// Mock implementation of control system for testing + /// + public class MockControlSystem : ICrestronControlSystem + { + public bool SupportsRelay { get; set; } = true; + public uint NumberOfRelayPorts { get; set; } = 8; + public Dictionary RelayPorts { get; set; } = new Dictionary(); + public Dictionary DigitalInputs { get; set; } = new Dictionary(); + public Dictionary VersiPorts { get; set; } = new Dictionary(); + public string ProgramIdTag { get; set; } = "TEST_PROGRAM"; + public string ControllerPrompt { get; set; } = "TEST>"; + public bool SupportsEthernet { get; set; } = true; + public bool SupportsDigitalInput { get; set; } = true; + public uint NumberOfDigitalInputPorts { get; set; } = 8; + public bool SupportsVersiPort { get; set; } = true; + public uint NumberOfVersiPorts { get; set; } = 8; + + public MockControlSystem() + { + // Initialize with default relay ports + for (uint i = 1; i <= NumberOfRelayPorts; i++) + { + RelayPorts[i] = new MockRelayPort(); + } + + // Initialize with default digital inputs + for (uint i = 1; i <= NumberOfDigitalInputPorts; i++) + { + DigitalInputs[i] = new MockDigitalInput(); + } + + // Initialize with default versiports + for (uint i = 1; i <= NumberOfVersiPorts; i++) + { + VersiPorts[i] = new MockVersiPort(); + } + } + } + + /// + /// Mock implementation of relay port for testing + /// + public class MockRelayPort : IRelayPort + { + private bool _state; + + public bool State => _state; + + public event EventHandler StateChanged; + + public void Open() + { + _state = false; + StateChanged?.Invoke(this, _state); + } + + public void Close() + { + _state = true; + StateChanged?.Invoke(this, _state); + } + + public void Pulse(int delayMs) + { + Close(); + System.Threading.Tasks.Task.Delay(delayMs).ContinueWith(_ => Open()); + } + + /// + /// Test helper to set state directly + /// + public void SetState(bool state) + { + _state = state; + StateChanged?.Invoke(this, _state); + } + } + + /// + /// Mock implementation of digital input for testing + /// + public class MockDigitalInput : IDigitalInput + { + private bool _state; + + public bool State => _state; + + public event EventHandler StateChange; + + /// + /// Test helper to simulate input change + /// + public void SimulateStateChange(bool newState) + { + _state = newState; + StateChange?.Invoke(this, new Abstractions.DigitalInputEventArgs(newState)); + } + } + + /// + /// Mock implementation of versiport for testing + /// + public class MockVersiPort : IVersiPort + { + private bool _digitalIn; + private bool _digitalOut; + private ushort _analogIn; + + public bool DigitalIn => _digitalIn; + public ushort AnalogIn => _analogIn; + + public event EventHandler VersiportChange; + + public void SetDigitalOut(bool value) + { + _digitalOut = value; + } + + /// + /// Test helper to simulate digital input change + /// + public void SimulateDigitalInChange(bool value) + { + _digitalIn = value; + VersiportChange?.Invoke(this, new Abstractions.VersiPortEventArgs + { + EventType = Abstractions.VersiPortEventType.DigitalInChange, + Value = value + }); + } + + /// + /// Test helper to simulate analog input change + /// + public void SimulateAnalogInChange(ushort value) + { + _analogIn = value; + VersiportChange?.Invoke(this, new Abstractions.VersiPortEventArgs + { + EventType = Abstractions.VersiPortEventType.AnalogInChange, + Value = value + }); + } + } + + /// + /// Mock implementation of console manager for testing + /// + public class MockConsoleManager : IConsoleManager + { + public List OutputLines { get; } = new List(); + public Dictionary> Commands { get; } = new Dictionary>(); + + public void Print(string message) + { + OutputLines.Add(message); + Console.Write(message); + } + + public void PrintLine(string message) + { + OutputLines.Add(message); + Console.WriteLine(message); + } + + public void RegisterCommand(string command, Action handler, string help) + { + Commands[command] = handler; + } + + /// + /// Test helper to execute a registered command + /// + public void ExecuteCommand(string command, string args) + { + if (Commands.TryGetValue(command, out var handler)) + { + handler(args); + } + } + } + + /// + /// Mock implementation of system info for testing + /// + public class MockSystemInfo : ISystemInfo + { + public string ProgramName { get; set; } = "TestProgram"; + public string SerialNumber { get; set; } = "TEST123456"; + public string MacAddress { get; set; } = "00:11:22:33:44:55"; + public string IpAddress { get; set; } = "192.168.1.100"; + public string FirmwareVersion { get; set; } = "1.0.0.0"; + public DateTime SystemUpTime { get; set; } = DateTime.Now.AddHours(-1); + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Factory/CrestronRuntimeProvider.cs b/src/PepperDash.Essentials.Core/Factory/CrestronRuntimeProvider.cs new file mode 100644 index 00000000..0c6e0f31 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Factory/CrestronRuntimeProvider.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using PepperDash.Essentials.Core.Abstractions; + +#if !TEST_BUILD +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +#endif + +namespace PepperDash.Essentials.Core.Factory +{ + /// + /// Runtime provider that uses actual Crestron libraries + /// + public class CrestronRuntimeProvider : ICrestronEnvironmentProvider + { + private ICrestronControlSystem _controlSystem; + + public ICrestronControlSystem GetControlSystem() + { + if (_controlSystem == null) + { +#if !TEST_BUILD + // In runtime, wrap the actual Crestron control system + // Note: This would need to be adapted based on the actual Crestron API + // For now, return a null implementation +#endif + { + // Return a null object pattern implementation for non-Crestron environments + _controlSystem = new NullControlSystem(); + } + } + return _controlSystem; + } + + public IRelayPort CreateRelayPort(uint portNumber) + { +#if !TEST_BUILD + var controlSystem = GetControlSystem(); + if (controlSystem.RelayPorts.TryGetValue(portNumber, out var port)) + { + return port; + } +#endif + return new NullRelayPort(); + } + + public IDigitalInput CreateDigitalInput(uint portNumber) + { +#if !TEST_BUILD + // Implementation would wrap actual Crestron digital input + // This is a simplified version +#endif + return new NullDigitalInput(); + } + + public IVersiPort CreateVersiPort(uint portNumber) + { +#if !TEST_BUILD + // Implementation would wrap actual Crestron versiport + // This is a simplified version +#endif + return new NullVersiPort(); + } + + public IConsoleManager GetConsoleManager() + { +#if !TEST_BUILD + return new CrestronConsoleManager(); +#else + return new NullConsoleManager(); +#endif + } + + public ISystemInfo GetSystemInfo() + { +#if !TEST_BUILD + return new CrestronSystemInfo(); +#else + return new NullSystemInfo(); +#endif + } + + #region Null Object Pattern Implementations + + private class NullControlSystem : ICrestronControlSystem + { + public bool SupportsRelay => false; + public uint NumberOfRelayPorts => 0; + public Dictionary RelayPorts => new Dictionary(); + public string ProgramIdTag => "NULL"; + public string ControllerPrompt => "NULL>"; + public bool SupportsEthernet => false; + public bool SupportsDigitalInput => false; + public uint NumberOfDigitalInputPorts => 0; + public bool SupportsVersiPort => false; + public uint NumberOfVersiPorts => 0; + } + + private class NullRelayPort : IRelayPort + { + public bool State => false; + public void Open() { } + public void Close() { } + public void Pulse(int delayMs) { } + } + + private class NullDigitalInput : IDigitalInput + { + public bool State => false; + public event EventHandler StateChange; + } + + private class NullVersiPort : IVersiPort + { + public bool DigitalIn => false; + public ushort AnalogIn => 0; + public void SetDigitalOut(bool value) { } + public event EventHandler VersiportChange; + } + + private class NullConsoleManager : IConsoleManager + { + public void Print(string message) { } + public void PrintLine(string message) { } + public void RegisterCommand(string command, Action handler, string help) { } + } + + private class NullSystemInfo : ISystemInfo + { + public string ProgramName => "NULL"; + public string SerialNumber => "000000"; + public string MacAddress => "00:00:00:00:00:00"; + public string IpAddress => "0.0.0.0"; + public string FirmwareVersion => "0.0.0"; + public DateTime SystemUpTime => DateTime.Now; + } + + #endregion + +#if !TEST_BUILD + private class CrestronConsoleManager : IConsoleManager + { + public void Print(string message) + { + CrestronConsole.Print(message); + } + + public void PrintLine(string message) + { + CrestronConsole.PrintLine(message); + } + + public void RegisterCommand(string command, Action handler, string help) + { + CrestronConsole.AddNewConsoleCommand((s) => handler(s), command, help, ConsoleAccessLevelEnum.AccessOperator); + } + } + + private class CrestronSystemInfo : ISystemInfo + { + public string ProgramName => "CrestronProgram"; + public string SerialNumber => "000000"; + public string MacAddress => "00:00:00:00:00:00"; + public string IpAddress => "0.0.0.0"; + public string FirmwareVersion => "1.0.0"; + public DateTime SystemUpTime => DateTime.Now; + } +#endif + } +} \ No newline at end of file diff --git a/tests/PepperDash.Essentials.Core.Tests/Devices/CrestronProcessorTestableTests.cs b/tests/PepperDash.Essentials.Core.Tests/Devices/CrestronProcessorTestableTests.cs new file mode 100644 index 00000000..32ca2413 --- /dev/null +++ b/tests/PepperDash.Essentials.Core.Tests/Devices/CrestronProcessorTestableTests.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using Xunit; +using FluentAssertions; +using Moq; +using PepperDash.Essentials.Core.Devices; +using PepperDash.Essentials.Core.Abstractions; +using PepperDash.Essentials.Core.Factory; +using PepperDash.Essentials.Core.CrestronIO; + +namespace PepperDash.Essentials.Core.Tests.Devices +{ + public class CrestronProcessorTestableTests : IDisposable + { + public CrestronProcessorTestableTests() + { + // Enable test mode for all tests in this class + CrestronEnvironmentFactory.EnableTestMode(); + } + + public void Dispose() + { + // Restore runtime mode after tests + CrestronEnvironmentFactory.DisableTestMode(); + } + + [Fact] + public void Constructor_WithNullProcessor_UsesFactoryToGetControlSystem() + { + // Arrange & Act + var processor = new CrestronProcessorTestable("test-processor"); + + // Assert + processor.Should().NotBeNull(); + processor.Processor.Should().NotBeNull(); + processor.Key.Should().Be("test-processor"); + } + + [Fact] + public void Constructor_WithProvidedProcessor_UsesProvidedProcessor() + { + // Arrange + var mockProcessor = new Mock(); + mockProcessor.Setup(p => p.SupportsRelay).Returns(false); + mockProcessor.Setup(p => p.RelayPorts).Returns(new Dictionary()); + + // Act + var processor = new CrestronProcessorTestable("test-processor", mockProcessor.Object); + + // Assert + processor.Processor.Should().BeSameAs(mockProcessor.Object); + } + + [Fact] + public void GetRelays_WhenProcessorSupportsRelays_CreatesRelayDevices() + { + // Arrange + var mockProvider = new CrestronMockProvider(); + mockProvider.ConfigureMockSystem(system => + { + system.SupportsRelay = true; + system.NumberOfRelayPorts = 4; + // Ensure relay ports are initialized + for (uint i = 1; i <= 4; i++) + { + system.RelayPorts[i] = new MockRelayPort(); + } + }); + + CrestronEnvironmentFactory.SetProvider(mockProvider); + + // Act + var processor = new CrestronProcessorTestable("test-processor"); + + // Assert + processor.SwitchedOutputs.Should().HaveCount(4); + processor.SwitchedOutputs.Should().ContainKeys(1, 2, 3, 4); + + foreach (var kvp in processor.SwitchedOutputs) + { + kvp.Value.Should().BeOfType(); + var relayDevice = kvp.Value as GenericRelayDeviceTestable; + relayDevice.Key.Should().Be($"test-processor-relay-{kvp.Key}"); + } + } + + [Fact] + public void GetRelays_WhenProcessorDoesNotSupportRelays_CreatesNoDevices() + { + // Arrange + var mockProcessor = new Mock(); + mockProcessor.Setup(p => p.SupportsRelay).Returns(false); + mockProcessor.Setup(p => p.RelayPorts).Returns(new Dictionary()); + + // Act + var processor = new CrestronProcessorTestable("test-processor", mockProcessor.Object); + + // Assert + processor.SwitchedOutputs.Should().BeEmpty(); + } + + [Fact] + public void GetRelays_HandlesExceptionGracefully() + { + // Arrange + var mockProcessor = new Mock(); + mockProcessor.Setup(p => p.SupportsRelay).Returns(true); + mockProcessor.Setup(p => p.NumberOfRelayPorts).Throws(new Exception("Test exception")); + mockProcessor.Setup(p => p.RelayPorts).Returns(new Dictionary()); + + // Act + Action act = () => new CrestronProcessorTestable("test-processor", mockProcessor.Object); + + // Assert + act.Should().NotThrow(); + } + } + + public class GenericRelayDeviceTestableTests : IDisposable + { + public GenericRelayDeviceTestableTests() + { + CrestronEnvironmentFactory.EnableTestMode(); + } + + public void Dispose() + { + CrestronEnvironmentFactory.DisableTestMode(); + } + + [Fact] + public void Constructor_WithNullRelayPort_ThrowsArgumentNullException() + { + // Act & Assert + Action act = () => new GenericRelayDeviceTestable("test-relay", null); + act.Should().Throw() + .WithParameterName("relayPort"); + } + + [Fact] + public void Constructor_WithValidRelayPort_InitializesCorrectly() + { + // Arrange + var mockRelayPort = new MockRelayPort(); + + // Act + var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort); + + // Assert + device.Should().NotBeNull(); + device.Key.Should().Be("test-relay"); + device.OutputIsOnFeedback.Should().NotBeNull(); + } + + [Fact] + public void OpenRelay_OpensRelayAndUpdatesFeedback() + { + // Arrange + var mockRelayPort = new MockRelayPort(); + mockRelayPort.SetState(true); // Start with closed relay + var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort); + + bool feedbackFired = false; + device.OutputIsOnFeedback.OutputChange += (sender, args) => feedbackFired = true; + + // Act + device.OpenRelay(); + + // Assert + mockRelayPort.State.Should().BeFalse(); + device.IsOn.Should().BeFalse(); + feedbackFired.Should().BeTrue(); + } + + [Fact] + public void CloseRelay_ClosesRelayAndUpdatesFeedback() + { + // Arrange + var mockRelayPort = new MockRelayPort(); + mockRelayPort.SetState(false); // Start with open relay + var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort); + + bool feedbackFired = false; + device.OutputIsOnFeedback.OutputChange += (sender, args) => feedbackFired = true; + + // Act + device.CloseRelay(); + + // Assert + mockRelayPort.State.Should().BeTrue(); + device.IsOn.Should().BeTrue(); + feedbackFired.Should().BeTrue(); + } + + [Fact] + public void PulseRelay_CallsPulseOnRelayPort() + { + // Arrange + var mockRelayPort = new Mock(); + var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort.Object); + + // Act + device.PulseRelay(500); + + // Assert + mockRelayPort.Verify(r => r.Pulse(500), Times.Once); + } + + [Fact] + public void On_ClosesRelay() + { + // Arrange + var mockRelayPort = new MockRelayPort(); + var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort); + + // Act + device.On(); + + // Assert + mockRelayPort.State.Should().BeTrue(); + device.IsOn.Should().BeTrue(); + } + + [Fact] + public void Off_OpensRelay() + { + // Arrange + var mockRelayPort = new MockRelayPort(); + mockRelayPort.SetState(true); // Start with closed relay + var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort); + + // Act + device.Off(); + + // Assert + mockRelayPort.State.Should().BeFalse(); + device.IsOn.Should().BeFalse(); + } + + [Fact] + public void PowerToggle_TogglesRelayState() + { + // Arrange + var mockRelayPort = new MockRelayPort(); + var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort); + + // Act & Assert - First toggle (off to on) + device.PowerToggle(); + mockRelayPort.State.Should().BeTrue(); + device.IsOn.Should().BeTrue(); + + // Act & Assert - Second toggle (on to off) + device.PowerToggle(); + mockRelayPort.State.Should().BeFalse(); + device.IsOn.Should().BeFalse(); + } + + [Fact] + public void IsOn_ReflectsRelayPortState() + { + // Arrange + var mockRelayPort = new MockRelayPort(); + var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort); + + // Act & Assert - Initially off + device.IsOn.Should().BeFalse(); + + // Act & Assert - After closing + mockRelayPort.Close(); + device.IsOn.Should().BeTrue(); + + // Act & Assert - After opening + mockRelayPort.Open(); + device.IsOn.Should().BeFalse(); + } + + [Fact] + public void CustomActivate_ReinitializesFeedback() + { + // Arrange + var mockRelayPort = new MockRelayPort(); + var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort); + var originalFeedback = device.OutputIsOnFeedback; + + // Act + var result = device.CustomActivate(); + + // Assert + result.Should().BeTrue(); + device.OutputIsOnFeedback.Should().NotBeNull(); + device.OutputIsOnFeedback.Should().NotBeSameAs(originalFeedback); + } + } + + public class IntegrationTests : IDisposable + { + public IntegrationTests() + { + CrestronEnvironmentFactory.EnableTestMode(); + } + + public void Dispose() + { + CrestronEnvironmentFactory.DisableTestMode(); + } + + [Fact] + public void FullSystemIntegration_CreatesAndControlsRelays() + { + // Arrange + var mockProvider = new CrestronMockProvider(); + mockProvider.ConfigureMockSystem(system => + { + system.SupportsRelay = true; + system.NumberOfRelayPorts = 2; + system.ProgramIdTag = "INTEGRATION_TEST"; + }); + + CrestronEnvironmentFactory.SetProvider(mockProvider); + + // Act + var processor = new CrestronProcessorTestable("integration-processor"); + + // Assert processor creation + processor.Processor.ProgramIdTag.Should().Be("INTEGRATION_TEST"); + processor.SwitchedOutputs.Should().HaveCount(2); + + // Test relay control + var relay1 = processor.SwitchedOutputs[1] as GenericRelayDeviceTestable; + relay1.Should().NotBeNull(); + + // Test On/Off operations + relay1.On(); + relay1.IsOn.Should().BeTrue(); + + relay1.Off(); + relay1.IsOn.Should().BeFalse(); + + // Test toggle + relay1.PowerToggle(); + relay1.IsOn.Should().BeTrue(); + + relay1.PowerToggle(); + relay1.IsOn.Should().BeFalse(); + + // Test feedback + int feedbackCount = 0; + relay1.OutputIsOnFeedback.OutputChange += (sender, args) => feedbackCount++; + + relay1.On(); + feedbackCount.Should().Be(1); + + relay1.Off(); + feedbackCount.Should().Be(2); + } + + [Fact] + public void FactoryPattern_AllowsSwitchingBetweenProviders() + { + // Arrange - Start with test mode + CrestronEnvironmentFactory.EnableTestMode(); + CrestronEnvironmentFactory.IsTestMode.Should().BeTrue(); + + var testProcessor = new CrestronProcessorTestable("test-mode"); + testProcessor.Processor.Should().BeOfType(); + + // Act - Switch to runtime mode + CrestronEnvironmentFactory.DisableTestMode(); + CrestronEnvironmentFactory.IsTestMode.Should().BeFalse(); + + // Note: In runtime mode without actual Crestron hardware, + // it will use the NullControlSystem implementation + var runtimeProcessor = new CrestronProcessorTestable("runtime-mode"); + runtimeProcessor.Processor.Should().NotBeNull(); + + // Act - Switch back to test mode + CrestronEnvironmentFactory.EnableTestMode(); + CrestronEnvironmentFactory.IsTestMode.Should().BeTrue(); + + var testProcessor2 = new CrestronProcessorTestable("test-mode-2"); + testProcessor2.Processor.Should().BeOfType(); + } + } +} \ No newline at end of file diff --git a/tools/CrestronMockGenerator/AssemblyAnalyzer.cs b/tools/CrestronMockGenerator/AssemblyAnalyzer.cs new file mode 100644 index 00000000..599887c5 --- /dev/null +++ b/tools/CrestronMockGenerator/AssemblyAnalyzer.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using System.Xml.Linq; +using Newtonsoft.Json; + +namespace CrestronMockGenerator +{ + public class AssemblyAnalyzer + { + private readonly string _assemblyPath; + private readonly string _xmlDocPath; + private Dictionary _xmlDocumentation = new(); + + public AssemblyAnalyzer(string assemblyPath) + { + _assemblyPath = assemblyPath; + _xmlDocPath = Path.ChangeExtension(assemblyPath, ".xml"); + + if (File.Exists(_xmlDocPath)) + { + LoadXmlDocumentation(); + } + } + + private void LoadXmlDocumentation() + { + try + { + var doc = XDocument.Load(_xmlDocPath); + var members = doc.Descendants("member"); + + foreach (var member in members) + { + var name = member.Attribute("name")?.Value; + var summary = member.Element("summary")?.Value?.Trim(); + + if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(summary)) + { + _xmlDocumentation[name] = summary; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error loading XML documentation: {ex.Message}"); + } + } + + public AssemblyInfo AnalyzeAssembly() + { + var assemblyInfo = new AssemblyInfo + { + Name = Path.GetFileNameWithoutExtension(_assemblyPath), + Types = new List() + }; + + try + { + // Use MetadataLoadContext to load assembly without dependencies + var resolver = new PathAssemblyResolver(new[] { _assemblyPath }); + var mlc = new MetadataLoadContext(resolver); + + var assembly = mlc.LoadFromAssemblyPath(_assemblyPath); + + foreach (var type in assembly.GetExportedTypes()) + { + if (type.IsSpecialName || type.Name.Contains("<>")) + continue; + + var typeInfo = AnalyzeType(type); + if (typeInfo != null) + { + assemblyInfo.Types.Add(typeInfo); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error analyzing assembly: {ex.Message}"); + } + + return assemblyInfo; + } + + private TypeInfo? AnalyzeType(Type type) + { + try + { + var typeInfo = new TypeInfo + { + Name = type.Name, + Namespace = type.Namespace ?? "", + FullName = type.FullName ?? "", + IsInterface = type.IsInterface, + IsAbstract = type.IsAbstract, + IsSealed = type.IsSealed, + IsEnum = type.IsEnum, + IsClass = type.IsClass, + BaseType = type.BaseType?.FullName, + Documentation = GetDocumentation($"T:{type.FullName}"), + Properties = new List(), + Methods = new List(), + Events = new List(), + Fields = new List(), + Interfaces = type.GetInterfaces().Select(i => i.FullName).Where(n => n != null).ToList() + }; + + // Analyze properties + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) + { + typeInfo.Properties.Add(new PropertyInfo + { + Name = prop.Name, + Type = prop.PropertyType.FullName ?? "", + CanRead = prop.CanRead, + CanWrite = prop.CanWrite, + IsStatic = prop.GetMethod?.IsStatic ?? prop.SetMethod?.IsStatic ?? false, + Documentation = GetDocumentation($"P:{type.FullName}.{prop.Name}") + }); + } + + // Analyze methods + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .Where(m => !m.IsSpecialName)) + { + var parameters = method.GetParameters().Select(p => new ParameterInfo + { + Name = p.Name ?? "", + Type = p.ParameterType.FullName ?? "", + IsOut = p.IsOut, + IsRef = p.ParameterType.IsByRef && !p.IsOut, + HasDefaultValue = p.HasDefaultValue, + DefaultValue = p.HasDefaultValue ? p.DefaultValue?.ToString() : null + }).ToList(); + + typeInfo.Methods.Add(new MethodInfo + { + Name = method.Name, + ReturnType = method.ReturnType.FullName ?? "", + IsStatic = method.IsStatic, + IsVirtual = method.IsVirtual, + IsAbstract = method.IsAbstract, + Parameters = parameters, + Documentation = GetDocumentation($"M:{type.FullName}.{method.Name}") + }); + } + + // Analyze events + foreach (var evt in type.GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) + { + typeInfo.Events.Add(new EventInfo + { + Name = evt.Name, + EventHandlerType = evt.EventHandlerType?.FullName ?? "", + Documentation = GetDocumentation($"E:{type.FullName}.{evt.Name}") + }); + } + + // Analyze fields (for enums) + if (type.IsEnum) + { + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static)) + { + if (field.Name == "value__") continue; + + typeInfo.Fields.Add(new FieldInfo + { + Name = field.Name, + Type = field.FieldType.FullName ?? "", + Value = field.GetRawConstantValue()?.ToString(), + Documentation = GetDocumentation($"F:{type.FullName}.{field.Name}") + }); + } + } + + return typeInfo; + } + catch (Exception ex) + { + Console.WriteLine($"Error analyzing type {type.Name}: {ex.Message}"); + return null; + } + } + + private string GetDocumentation(string memberName) + { + return _xmlDocumentation.TryGetValue(memberName, out var doc) ? doc : ""; + } + } + + public class AssemblyInfo + { + public string Name { get; set; } = ""; + public List Types { get; set; } = new(); + } + + public class TypeInfo + { + public string Name { get; set; } = ""; + public string Namespace { get; set; } = ""; + public string FullName { get; set; } = ""; + public bool IsInterface { get; set; } + public bool IsAbstract { get; set; } + public bool IsSealed { get; set; } + public bool IsEnum { get; set; } + public bool IsClass { get; set; } + public string? BaseType { get; set; } + public List Interfaces { get; set; } = new(); + public string Documentation { get; set; } = ""; + public List Properties { get; set; } = new(); + public List Methods { get; set; } = new(); + public List Events { get; set; } = new(); + public List Fields { get; set; } = new(); + } + + public class PropertyInfo + { + public string Name { get; set; } = ""; + public string Type { get; set; } = ""; + public bool CanRead { get; set; } + public bool CanWrite { get; set; } + public bool IsStatic { get; set; } + public string Documentation { get; set; } = ""; + } + + public class MethodInfo + { + public string Name { get; set; } = ""; + public string ReturnType { get; set; } = ""; + public bool IsStatic { get; set; } + public bool IsVirtual { get; set; } + public bool IsAbstract { get; set; } + public List Parameters { get; set; } = new(); + public string Documentation { get; set; } = ""; + } + + public class ParameterInfo + { + public string Name { get; set; } = ""; + public string Type { get; set; } = ""; + public bool IsOut { get; set; } + public bool IsRef { get; set; } + public bool HasDefaultValue { get; set; } + public string? DefaultValue { get; set; } + } + + public class EventInfo + { + public string Name { get; set; } = ""; + public string EventHandlerType { get; set; } = ""; + public string Documentation { get; set; } = ""; + } + + public class FieldInfo + { + public string Name { get; set; } = ""; + public string Type { get; set; } = ""; + public string? Value { get; set; } + public string Documentation { get; set; } = ""; + } +} \ No newline at end of file diff --git a/tools/CrestronMockGenerator/CrestronMockGenerator.csproj b/tools/CrestronMockGenerator/CrestronMockGenerator.csproj new file mode 100644 index 00000000..93ae6ae3 --- /dev/null +++ b/tools/CrestronMockGenerator/CrestronMockGenerator.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + \ No newline at end of file diff --git a/tools/CrestronMockGenerator/MockGenerator.cs b/tools/CrestronMockGenerator/MockGenerator.cs new file mode 100644 index 00000000..89febb63 --- /dev/null +++ b/tools/CrestronMockGenerator/MockGenerator.cs @@ -0,0 +1,468 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CrestronMockGenerator +{ + public class MockGenerator + { + public string GenerateMockClass(TypeInfo typeInfo) + { + var compilationUnit = CompilationUnit(); + + // Add usings + compilationUnit = compilationUnit.AddUsings( + UsingDirective(ParseName("System")), + UsingDirective(ParseName("System.Collections.Generic")), + UsingDirective(ParseName("System.Linq")) + ); + + // Create namespace + var namespaceDeclaration = NamespaceDeclaration(ParseName(typeInfo.Namespace)); + + // Add XML documentation if available + var trivia = new List(); + if (!string.IsNullOrEmpty(typeInfo.Documentation)) + { + trivia.Add(Comment($"/// ")); + trivia.Add(Comment($"/// {typeInfo.Documentation}")); + trivia.Add(Comment($"/// ")); + } + + // Create type declaration + MemberDeclarationSyntax typeDeclaration; + + if (typeInfo.IsEnum) + { + typeDeclaration = GenerateEnum(typeInfo); + } + else if (typeInfo.IsInterface) + { + typeDeclaration = GenerateInterface(typeInfo); + } + else + { + typeDeclaration = GenerateClass(typeInfo); + } + + if (trivia.Any()) + { + typeDeclaration = typeDeclaration.WithLeadingTrivia(trivia); + } + + namespaceDeclaration = namespaceDeclaration.AddMembers(typeDeclaration); + compilationUnit = compilationUnit.AddMembers(namespaceDeclaration); + + // Format the code + var workspace = new AdhocWorkspace(); + var formattedNode = Microsoft.CodeAnalysis.Formatting.Formatter.Format( + compilationUnit, + workspace); + + return formattedNode.ToFullString(); + } + + private EnumDeclarationSyntax GenerateEnum(TypeInfo typeInfo) + { + var enumDeclaration = EnumDeclaration(typeInfo.Name) + .AddModifiers(Token(SyntaxKind.PublicKeyword)); + + foreach (var field in typeInfo.Fields) + { + var member = EnumMemberDeclaration(field.Name); + + if (!string.IsNullOrEmpty(field.Value)) + { + member = member.WithEqualsValue( + EqualsValueClause(ParseExpression(field.Value))); + } + + if (!string.IsNullOrEmpty(field.Documentation)) + { + member = member.WithLeadingTrivia( + Comment($"/// "), + Comment($"/// {field.Documentation}"), + Comment($"/// ")); + } + + enumDeclaration = enumDeclaration.AddMembers(member); + } + + return enumDeclaration; + } + + private InterfaceDeclarationSyntax GenerateInterface(TypeInfo typeInfo) + { + var interfaceDeclaration = InterfaceDeclaration(typeInfo.Name) + .AddModifiers(Token(SyntaxKind.PublicKeyword)); + + // Add base interfaces + if (typeInfo.Interfaces.Any()) + { + var baseList = BaseList(); + foreach (var baseInterface in typeInfo.Interfaces) + { + var typeName = GetSimpleTypeName(baseInterface); + baseList = baseList.AddTypes(SimpleBaseType(ParseTypeName(typeName))); + } + interfaceDeclaration = interfaceDeclaration.WithBaseList(baseList); + } + + // Add properties + foreach (var property in typeInfo.Properties) + { + var propertyDeclaration = GenerateInterfaceProperty(property); + interfaceDeclaration = interfaceDeclaration.AddMembers(propertyDeclaration); + } + + // Add methods + foreach (var method in typeInfo.Methods) + { + var methodDeclaration = GenerateInterfaceMethod(method); + interfaceDeclaration = interfaceDeclaration.AddMembers(methodDeclaration); + } + + // Add events + foreach (var evt in typeInfo.Events) + { + var eventDeclaration = GenerateInterfaceEvent(evt); + interfaceDeclaration = interfaceDeclaration.AddMembers(eventDeclaration); + } + + return interfaceDeclaration; + } + + private ClassDeclarationSyntax GenerateClass(TypeInfo typeInfo) + { + var classDeclaration = ClassDeclaration(typeInfo.Name) + .AddModifiers(Token(SyntaxKind.PublicKeyword)); + + if (typeInfo.IsAbstract) + { + classDeclaration = classDeclaration.AddModifiers(Token(SyntaxKind.AbstractKeyword)); + } + else if (typeInfo.IsSealed) + { + classDeclaration = classDeclaration.AddModifiers(Token(SyntaxKind.SealedKeyword)); + } + + // Add base class and interfaces + var baseTypes = new List(); + if (!string.IsNullOrEmpty(typeInfo.BaseType) && typeInfo.BaseType != "System.Object") + { + baseTypes.Add(typeInfo.BaseType); + } + baseTypes.AddRange(typeInfo.Interfaces); + + if (baseTypes.Any()) + { + var baseList = BaseList(); + foreach (var baseType in baseTypes) + { + var typeName = GetSimpleTypeName(baseType); + baseList = baseList.AddTypes(SimpleBaseType(ParseTypeName(typeName))); + } + classDeclaration = classDeclaration.WithBaseList(baseList); + } + + // Add properties + foreach (var property in typeInfo.Properties) + { + var propertyDeclaration = GenerateProperty(property); + classDeclaration = classDeclaration.AddMembers(propertyDeclaration); + } + + // Add methods + foreach (var method in typeInfo.Methods) + { + var methodDeclaration = GenerateMethod(method, typeInfo.IsAbstract); + classDeclaration = classDeclaration.AddMembers(methodDeclaration); + } + + // Add events + foreach (var evt in typeInfo.Events) + { + var eventDeclaration = GenerateEvent(evt); + classDeclaration = classDeclaration.AddMembers(eventDeclaration); + } + + return classDeclaration; + } + + private PropertyDeclarationSyntax GenerateProperty(PropertyInfo property) + { + var typeName = GetSimpleTypeName(property.Type); + var propertyDeclaration = PropertyDeclaration(ParseTypeName(typeName), property.Name) + .AddModifiers(Token(SyntaxKind.PublicKeyword)); + + if (property.IsStatic) + { + propertyDeclaration = propertyDeclaration.AddModifiers(Token(SyntaxKind.StaticKeyword)); + } + + var accessors = new List(); + + if (property.CanRead) + { + accessors.Add(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))); + } + + if (property.CanWrite) + { + accessors.Add(AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))); + } + + propertyDeclaration = propertyDeclaration.WithAccessorList( + AccessorList(List(accessors))); + + if (!string.IsNullOrEmpty(property.Documentation)) + { + propertyDeclaration = propertyDeclaration.WithLeadingTrivia( + Comment($"/// "), + Comment($"/// {property.Documentation}"), + Comment($"/// ")); + } + + return propertyDeclaration; + } + + private PropertyDeclarationSyntax GenerateInterfaceProperty(PropertyInfo property) + { + var typeName = GetSimpleTypeName(property.Type); + var propertyDeclaration = PropertyDeclaration(ParseTypeName(typeName), property.Name); + + var accessors = new List(); + + if (property.CanRead) + { + accessors.Add(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))); + } + + if (property.CanWrite) + { + accessors.Add(AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))); + } + + propertyDeclaration = propertyDeclaration.WithAccessorList( + AccessorList(List(accessors))); + + if (!string.IsNullOrEmpty(property.Documentation)) + { + propertyDeclaration = propertyDeclaration.WithLeadingTrivia( + Comment($"/// "), + Comment($"/// {property.Documentation}"), + Comment($"/// ")); + } + + return propertyDeclaration; + } + + private MethodDeclarationSyntax GenerateMethod(MethodInfo method, bool isAbstractClass) + { + var returnTypeName = GetSimpleTypeName(method.ReturnType); + var methodDeclaration = MethodDeclaration(ParseTypeName(returnTypeName), method.Name) + .AddModifiers(Token(SyntaxKind.PublicKeyword)); + + if (method.IsStatic) + { + methodDeclaration = methodDeclaration.AddModifiers(Token(SyntaxKind.StaticKeyword)); + } + else if (method.IsAbstract && isAbstractClass) + { + methodDeclaration = methodDeclaration.AddModifiers(Token(SyntaxKind.AbstractKeyword)); + } + else if (method.IsVirtual) + { + methodDeclaration = methodDeclaration.AddModifiers(Token(SyntaxKind.VirtualKeyword)); + } + + // Add parameters + var parameters = new List(); + foreach (var param in method.Parameters) + { + var paramTypeName = GetSimpleTypeName(param.Type); + var parameter = Parameter(Identifier(param.Name)) + .WithType(ParseTypeName(paramTypeName)); + + if (param.IsOut) + { + parameter = parameter.AddModifiers(Token(SyntaxKind.OutKeyword)); + } + else if (param.IsRef) + { + parameter = parameter.AddModifiers(Token(SyntaxKind.RefKeyword)); + } + + if (param.HasDefaultValue && !string.IsNullOrEmpty(param.DefaultValue)) + { + parameter = parameter.WithDefault( + EqualsValueClause(ParseExpression(param.DefaultValue))); + } + + parameters.Add(parameter); + } + + methodDeclaration = methodDeclaration.WithParameterList( + ParameterList(SeparatedList(parameters))); + + // Add body or semicolon + if (method.IsAbstract && isAbstractClass) + { + methodDeclaration = methodDeclaration.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + } + else + { + var statements = new List(); + + // Add default implementation + if (returnTypeName != "void") + { + statements.Add(ParseStatement($"throw new NotImplementedException();")); + } + else + { + statements.Add(ParseStatement($"// Mock implementation")); + } + + methodDeclaration = methodDeclaration.WithBody(Block(statements)); + } + + if (!string.IsNullOrEmpty(method.Documentation)) + { + methodDeclaration = methodDeclaration.WithLeadingTrivia( + Comment($"/// "), + Comment($"/// {method.Documentation}"), + Comment($"/// ")); + } + + return methodDeclaration; + } + + private MethodDeclarationSyntax GenerateInterfaceMethod(MethodInfo method) + { + var returnTypeName = GetSimpleTypeName(method.ReturnType); + var methodDeclaration = MethodDeclaration(ParseTypeName(returnTypeName), method.Name); + + // Add parameters + var parameters = new List(); + foreach (var param in method.Parameters) + { + var paramTypeName = GetSimpleTypeName(param.Type); + var parameter = Parameter(Identifier(param.Name)) + .WithType(ParseTypeName(paramTypeName)); + + if (param.IsOut) + { + parameter = parameter.AddModifiers(Token(SyntaxKind.OutKeyword)); + } + else if (param.IsRef) + { + parameter = parameter.AddModifiers(Token(SyntaxKind.RefKeyword)); + } + + parameters.Add(parameter); + } + + methodDeclaration = methodDeclaration + .WithParameterList(ParameterList(SeparatedList(parameters))) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + + if (!string.IsNullOrEmpty(method.Documentation)) + { + methodDeclaration = methodDeclaration.WithLeadingTrivia( + Comment($"/// "), + Comment($"/// {method.Documentation}"), + Comment($"/// ")); + } + + return methodDeclaration; + } + + private EventFieldDeclarationSyntax GenerateEvent(EventInfo evt) + { + var typeName = GetSimpleTypeName(evt.EventHandlerType); + var eventDeclaration = EventFieldDeclaration( + VariableDeclaration(ParseTypeName(typeName)) + .AddVariables(VariableDeclarator(evt.Name))) + .AddModifiers(Token(SyntaxKind.PublicKeyword)); + + if (!string.IsNullOrEmpty(evt.Documentation)) + { + eventDeclaration = eventDeclaration.WithLeadingTrivia( + Comment($"/// "), + Comment($"/// {evt.Documentation}"), + Comment($"/// ")); + } + + return eventDeclaration; + } + + private EventFieldDeclarationSyntax GenerateInterfaceEvent(EventInfo evt) + { + var typeName = GetSimpleTypeName(evt.EventHandlerType); + var eventDeclaration = EventFieldDeclaration( + VariableDeclaration(ParseTypeName(typeName)) + .AddVariables(VariableDeclarator(evt.Name))); + + if (!string.IsNullOrEmpty(evt.Documentation)) + { + eventDeclaration = eventDeclaration.WithLeadingTrivia( + Comment($"/// "), + Comment($"/// {evt.Documentation}"), + Comment($"/// ")); + } + + return eventDeclaration; + } + + private string GetSimpleTypeName(string fullTypeName) + { + // Convert full type names to simple names + var typeMappings = new Dictionary + { + ["System.Void"] = "void", + ["System.String"] = "string", + ["System.Int32"] = "int", + ["System.Int64"] = "long", + ["System.Int16"] = "short", + ["System.UInt32"] = "uint", + ["System.UInt64"] = "ulong", + ["System.UInt16"] = "ushort", + ["System.Byte"] = "byte", + ["System.SByte"] = "sbyte", + ["System.Boolean"] = "bool", + ["System.Single"] = "float", + ["System.Double"] = "double", + ["System.Decimal"] = "decimal", + ["System.Object"] = "object", + ["System.Char"] = "char" + }; + + if (typeMappings.TryGetValue(fullTypeName, out var simpleName)) + { + return simpleName; + } + + // Handle generic types + if (fullTypeName.Contains('`')) + { + // Simplified generic handling + return "object"; + } + + // Return last part of the type name + var lastDot = fullTypeName.LastIndexOf('.'); + return lastDot >= 0 ? fullTypeName.Substring(lastDot + 1) : fullTypeName; + } + } +} \ No newline at end of file diff --git a/tools/CrestronMockGenerator/Program.cs b/tools/CrestronMockGenerator/Program.cs new file mode 100644 index 00000000..0a3f3efb --- /dev/null +++ b/tools/CrestronMockGenerator/Program.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using Newtonsoft.Json; + +namespace CrestronMockGenerator +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Crestron Mock Library Generator"); + Console.WriteLine("================================"); + + if (args.Length < 2) + { + Console.WriteLine("Usage: CrestronMockGenerator "); + Console.WriteLine("Example: CrestronMockGenerator /path/to/SimplSharp.dll /output/mocks"); + return; + } + + var inputPath = args[0]; + var outputDir = args[1]; + + if (!File.Exists(inputPath)) + { + Console.WriteLine($"Error: Assembly file not found: {inputPath}"); + return; + } + + Directory.CreateDirectory(outputDir); + + try + { + // Analyze the assembly + Console.WriteLine($"Analyzing assembly: {inputPath}"); + var analyzer = new AssemblyAnalyzer(inputPath); + var assemblyInfo = analyzer.AnalyzeAssembly(); + + Console.WriteLine($"Found {assemblyInfo.Types.Count} types"); + + // Save assembly metadata as JSON + var metadataPath = Path.Combine(outputDir, $"{assemblyInfo.Name}.metadata.json"); + var json = JsonConvert.SerializeObject(assemblyInfo, Formatting.Indented); + File.WriteAllText(metadataPath, json); + Console.WriteLine($"Saved metadata to: {metadataPath}"); + + // Generate mock classes + var generator = new MockGenerator(); + var generatedFiles = new List(); + + // Group types by namespace + var namespaceGroups = assemblyInfo.Types.GroupBy(t => t.Namespace); + + foreach (var namespaceGroup in namespaceGroups) + { + var namespacePath = Path.Combine(outputDir, namespaceGroup.Key.Replace('.', Path.DirectorySeparatorChar)); + Directory.CreateDirectory(namespacePath); + + foreach (var typeInfo in namespaceGroup) + { + try + { + Console.WriteLine($"Generating mock for: {typeInfo.FullName}"); + var mockCode = generator.GenerateMockClass(typeInfo); + + var fileName = $"{typeInfo.Name}.cs"; + var filePath = Path.Combine(namespacePath, fileName); + + File.WriteAllText(filePath, mockCode); + generatedFiles.Add(filePath); + } + catch (Exception ex) + { + Console.WriteLine($"Error generating mock for {typeInfo.Name}: {ex.Message}"); + } + } + } + + // Generate project file + GenerateProjectFile(outputDir, assemblyInfo.Name); + + Console.WriteLine($"\nGeneration complete!"); + Console.WriteLine($"Generated {generatedFiles.Count} mock files"); + Console.WriteLine($"Output directory: {outputDir}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } + } + + static void GenerateProjectFile(string outputDir, string assemblyName) + { + var projectContent = @" + + + net8.0 + " + assemblyName + @".Mock + " + assemblyName + @" + true + " + assemblyName + @".Mock + 1.0.0 + Mock implementation of " + assemblyName + @" for testing + + +"; + + var projectPath = Path.Combine(outputDir, $"{assemblyName}.Mock.csproj"); + File.WriteAllText(projectPath, projectContent); + Console.WriteLine($"Generated project file: {projectPath}"); + } + } +} \ No newline at end of file