using Moq; using System; using System.Collections.Generic; using System.IO; using System.Windows; using Xunit; using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Services.Debug; namespace XplorePlane.Tests.Services { /// /// 测试 DebugPanelConfigService(任务 1.2) /// Tests DebugPanelConfigService (Task 1.2) /// /// **Validates: Requirements 12.1-12.7** /// public class DebugPanelConfigServiceTests : IDisposable { private readonly string _tempDir; private readonly string _configPath; private readonly Mock _mockLogger; private readonly Mock _mockModuleLogger; private readonly DebugPanelConfigService _service; public DebugPanelConfigServiceTests() { // 创建临时测试目录 _tempDir = Path.Combine(Path.GetTempPath(), "XplorePlaneTests", Guid.NewGuid().ToString()); Directory.CreateDirectory(_tempDir); _configPath = Path.Combine(_tempDir, "DebugPanel.config"); // 设置 mock logger _mockModuleLogger = new Mock(); _mockLogger = new Mock(); _mockLogger.Setup(l => l.ForModule()) .Returns(_mockModuleLogger.Object); // 使用反射创建服务实例并设置配置路径 _service = new DebugPanelConfigService(_mockLogger.Object); // 使用反射修改私有字段 _configPath var field = typeof(DebugPanelConfigService).GetField("_configPath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); field?.SetValue(_service, _configPath); } public void Dispose() { // 清理临时目录 if (Directory.Exists(_tempDir)) { Directory.Delete(_tempDir, true); } } /// /// 测试:当配置文件不存在时,LoadConfig 应返回默认配置 /// **Validates: Requirement 12.7** /// [Fact] public void LoadConfig_WhenFileDoesNotExist_ReturnsDefaultConfig() { // Act var config = _service.LoadConfig(); // Assert Assert.NotNull(config); Assert.NotNull(config.Window); Assert.Equal(1200, config.Window.Width); Assert.Equal(800, config.Window.Height); Assert.Equal(WindowState.Normal, config.Window.State); Assert.NotNull(config.EventFilters); Assert.Equal(8, config.EventFilters.Count); Assert.True(config.EventFilters["MotionState"]); Assert.True(config.EventFilters["RaySourceState"]); Assert.True(config.EventFilters["DetectorState"]); Assert.True(config.EventFilters["SystemState"]); Assert.True(config.EventFilters["CameraState"]); Assert.True(config.EventFilters["LinkedViewState"]); Assert.True(config.EventFilters["RecipeExecutionState"]); Assert.True(config.EventFilters["CalibrationMatrix"]); } /// /// 测试:SaveConfig 应成功保存配置到 JSON 文件 /// **Validates: Requirements 12.1-12.5** /// [Fact] public void SaveConfig_WithValidConfig_SavesSuccessfully() { // Arrange var config = new DebugPanelConfig { Window = new WindowConfig { Left = 150, Top = 200, Width = 1400, Height = 900, State = WindowState.Maximized }, EventFilters = new Dictionary { ["MotionState"] = false, ["RaySourceState"] = true }, DockingLayout = "Test" }; // Act _service.SaveConfig(config); // Assert Assert.True(File.Exists(_configPath)); var json = File.ReadAllText(_configPath); Assert.Contains("\"Width\": 1400", json); Assert.Contains("\"Height\": 900", json); Assert.Contains("\"State\": 2", json); // WindowState.Maximized = 2 Assert.Contains("\"MotionState\": false", json); Assert.Contains("\"RaySourceState\": true", json); Assert.Contains("\"DockingLayout\": \"Test\"", json); } /// /// 测试:LoadConfig 应成功从 JSON 文件加载配置 /// **Validates: Requirement 12.6** /// [Fact] public void LoadConfig_WithExistingFile_LoadsSuccessfully() { // Arrange var originalConfig = new DebugPanelConfig { Window = new WindowConfig { Left = 250, Top = 300, Width = 1600, Height = 1000, State = WindowState.Normal }, EventFilters = new Dictionary { ["MotionState"] = true, ["RaySourceState"] = false, ["DetectorState"] = true }, DockingLayout = "Custom" }; _service.SaveConfig(originalConfig); // Act var loadedConfig = _service.LoadConfig(); // Assert Assert.NotNull(loadedConfig); Assert.NotNull(loadedConfig.Window); Assert.Equal(250, loadedConfig.Window.Left); Assert.Equal(300, loadedConfig.Window.Top); Assert.Equal(1600, loadedConfig.Window.Width); Assert.Equal(1000, loadedConfig.Window.Height); Assert.Equal(WindowState.Normal, loadedConfig.Window.State); Assert.NotNull(loadedConfig.EventFilters); Assert.True(loadedConfig.EventFilters["MotionState"]); Assert.False(loadedConfig.EventFilters["RaySourceState"]); Assert.True(loadedConfig.EventFilters["DetectorState"]); Assert.Equal("Custom", loadedConfig.DockingLayout); } /// /// 测试:SaveConfig 应创建不存在的目录 /// **Validates: Requirement 12.4** /// [Fact] public void SaveConfig_WhenDirectoryDoesNotExist_CreatesDirectory() { // Arrange var nestedPath = Path.Combine(_tempDir, "nested", "path", "DebugPanel.config"); var field = typeof(DebugPanelConfigService).GetField("_configPath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); field?.SetValue(_service, nestedPath); var config = new DebugPanelConfig { Window = new WindowConfig { Width = 1200, Height = 800, State = WindowState.Normal }, EventFilters = new Dictionary() }; // Act _service.SaveConfig(config); // Assert Assert.True(File.Exists(nestedPath)); Assert.True(Directory.Exists(Path.GetDirectoryName(nestedPath))); } /// /// 测试:SaveConfig 使用 null 配置应记录警告并不抛出异常 /// **Validates: Requirement 12.7** /// [Fact] public void SaveConfig_WithNullConfig_LogsWarningAndDoesNotThrow() { // Act & Assert var exception = Record.Exception(() => _service.SaveConfig(null)); Assert.Null(exception); // 验证记录了警告日志 _mockModuleLogger.Verify( l => l.Warn(It.IsAny(), It.IsAny()), Times.Once); } /// /// 测试:LoadConfig 遇到损坏的 JSON 文件应返回默认配置并记录警告 /// **Validates: Requirement 12.7** /// [Fact] public void LoadConfig_WithCorruptedFile_ReturnsDefaultConfigAndLogsWarning() { // Arrange File.WriteAllText(_configPath, "{ invalid json content }"); // Act var config = _service.LoadConfig(); // Assert Assert.NotNull(config); Assert.Equal(1200, config.Window.Width); Assert.Equal(800, config.Window.Height); // 验证记录了错误日志 _mockModuleLogger.Verify( l => l.Error(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } /// /// 测试:配置文件路径应使用 %AppData%\XplorePlane\DebugPanel.config /// **Validates: Requirement 12.4** /// [Fact] public void Constructor_SetsCorrectConfigPath() { // Arrange var freshService = new DebugPanelConfigService(_mockLogger.Object); var field = typeof(DebugPanelConfigService).GetField("_configPath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); // Act var actualPath = field?.GetValue(freshService) as string; // Assert Assert.NotNull(actualPath); var expectedPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XplorePlane", "DebugPanel.config"); Assert.Equal(expectedPath, actualPath); } /// /// 测试:配置文件应使用 JSON 格式 /// **Validates: Requirement 12.5** /// [Fact] public void SaveConfig_UsesJsonFormat() { // Arrange var config = new DebugPanelConfig { Window = new WindowConfig { Width = 1200, Height = 800, State = WindowState.Normal }, EventFilters = new Dictionary { ["Test"] = true } }; // Act _service.SaveConfig(config); // Assert var json = File.ReadAllText(_configPath); Assert.StartsWith("{", json.Trim()); Assert.EndsWith("}", json.Trim()); Assert.Contains("\"Window\":", json); Assert.Contains("\"EventFilters\":", json); } /// /// 测试:默认配置应包含所有 8 种状态类型的过滤器 /// **Validates: Requirement 12.3** /// [Fact] public void GetDefaultConfig_ContainsAllStateTypeFilters() { // Act var config = _service.LoadConfig(); // Assert Assert.NotNull(config.EventFilters); Assert.Equal(8, config.EventFilters.Count); Assert.Contains("MotionState", config.EventFilters.Keys); Assert.Contains("RaySourceState", config.EventFilters.Keys); Assert.Contains("DetectorState", config.EventFilters.Keys); Assert.Contains("SystemState", config.EventFilters.Keys); Assert.Contains("CameraState", config.EventFilters.Keys); Assert.Contains("LinkedViewState", config.EventFilters.Keys); Assert.Contains("RecipeExecutionState", config.EventFilters.Keys); Assert.Contains("CalibrationMatrix", config.EventFilters.Keys); } /// /// 测试:默认窗口配置应为 1200×800 像素 /// **Validates: Requirements 12.1** /// [Fact] public void GetDefaultConfig_HasCorrectWindowSize() { // Act var config = _service.LoadConfig(); // Assert Assert.Equal(1200, config.Window.Width); Assert.Equal(800, config.Window.Height); Assert.Equal(WindowState.Normal, config.Window.State); } /// /// 测试:构造函数应拒绝 null logger /// [Fact] public void Constructor_WithNullLogger_ThrowsArgumentNullException() { // Act & Assert Assert.Throws(() => new DebugPanelConfigService(null)); } } }