Concurrency crashes - H1 HardwareMonitorService.RebuildTree now builds the tree under _updateLock, so it can't enumerate a hardware's _active HashSet while the update loop mutates it. - A1 AppSettings now guards every dictionary read/write (and snapshots in Save) with a lock — safe under concurrent access from the parallel discovery threads. - R2 Plumbed that same lock (HardwareMonitorService.SensorReadLock) into RemoteWebServer and wrapped the Prometheus sensor.Values enumeration with it. - L1 Computer — refactored Add into AddCore, which performs the cancellation/enabled re-check and the _groups insertion atomically under _lock. A deferred task can no longer add (and leak) a group after Close() drained the list; if it loses the race it closes the group instead. - M1 UpdateTimer_Tick now bails before/after the await when _isShuttingDown is set in MainWindow_Closed, so an in-flight tick won't touch the disposed view-model/Computer. Broken behavior - T1 Tray callback now decodes NOTIFYICON_VERSION_4 correctly (message = LOWORD(lParam), icon id = HIWORD(lParam)) — right-click menu and double-click work again. - M2 A transient update exception no longer calls _timer.Stop(); the loop keeps running. - H2 Newly discovered (deferred) storage devices get the current ForceDriveWakeup setting applied in HardwareChanged. - V2 Sensor items carry a parent reference; toggling IsVisible recomputes the parent group's visibility, so no empty group headers. (Strengthened the existing test that had skipped this assertion.) - H3 Tree-rebuild coalescing now uses a dirty flag with a re-check, so a change arriving during a rebuild isn't lost. - R4 Web routing matches endpoints exactly on the query-stripped path (Url.AbsolutePath), so static assets like metrics.html aren't hijacked. - L8 IntelCpu.Update skips the bus/core-clock math while TimeStampCounterFrequency is still 0 (deferred-TSC window), so clocks keep their prior value instead of reporting 0 MHz. - V1 Existing plot series keep their assigned color; only an explicit user pen color updates them (no per-tick color shifting). - M3 Runtime errors write to a dedicated runtime.log (once), instead of overwriting the shared startup.log. - T2 CreateSensorIcon returns IntPtr.Zero on DIB failure instead of the shared main-icon handle (which callers DestroyIcon). I also set _isOpen = false in HardwareMonitorService.Dispose so the rebuild guard actually holds during shutdown (the latent after-close-rebuild issue adjacent to H3
187 lines
6.9 KiB
C#
187 lines
6.9 KiB
C#
using System;
|
|
using System.Linq;
|
|
using LibreHardwareMonitor.Hardware;
|
|
using LibreHardwareMonitor.Windows.WinUI.Services;
|
|
using LibreHardwareMonitor.Windows.WinUI.ViewModels;
|
|
using Moq;
|
|
using Xunit;
|
|
using Microsoft.UI.Xaml;
|
|
using Windows.UI;
|
|
|
|
namespace LibreHardwareMonitor.Windows.WinUI.Tests.ViewModels;
|
|
|
|
public class SensorTreeItemViewModelTests
|
|
{
|
|
private AppSettings CreateMockSettings()
|
|
{
|
|
return AppSettings.LoadDefault();
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateRoot_ReturnsRootKind()
|
|
{
|
|
var vm = SensorTreeItemViewModel.CreateRoot("Root Node");
|
|
Assert.Equal(SensorTreeItemKind.Root, vm.Kind);
|
|
Assert.Equal("Root Node", vm.Text);
|
|
Assert.Null(vm.Sensor);
|
|
Assert.Null(vm.Hardware);
|
|
Assert.Equal("\uE7F4", vm.IconGlyph);
|
|
Assert.True(vm.IsExpanded);
|
|
Assert.Empty(vm.Children);
|
|
}
|
|
|
|
[Fact]
|
|
public void Text_CanBeSetForRoot()
|
|
{
|
|
var vm = SensorTreeItemViewModel.CreateRoot("Root");
|
|
vm.Text = "New Root";
|
|
Assert.Equal("New Root", vm.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void FromHardware_CreatesHierarchy()
|
|
{
|
|
var mockHardware = new Mock<IHardware>();
|
|
mockHardware.Setup(h => h.Name).Returns("My Hardware");
|
|
mockHardware.Setup(h => h.HardwareType).Returns(HardwareType.Cpu);
|
|
mockHardware.Setup(h => h.Identifier).Returns(new Identifier("cpu", "0"));
|
|
|
|
var mockSensor = new Mock<ISensor>();
|
|
mockSensor.Setup(s => s.Name).Returns("Core 0");
|
|
mockSensor.Setup(s => s.SensorType).Returns(SensorType.Temperature);
|
|
mockSensor.Setup(s => s.Index).Returns(0);
|
|
mockSensor.Setup(s => s.Identifier).Returns(new Identifier("cpu", "0", "temperature", "0"));
|
|
|
|
mockHardware.Setup(h => h.Sensors).Returns(new[] { mockSensor.Object });
|
|
mockHardware.Setup(h => h.SubHardware).Returns(Array.Empty<IHardware>());
|
|
|
|
var settings = CreateMockSettings();
|
|
var vm = SensorTreeItemViewModel.FromHardware(mockHardware.Object, settings);
|
|
|
|
Assert.Equal(SensorTreeItemKind.Hardware, vm.Kind);
|
|
Assert.Equal("My Hardware", vm.Text);
|
|
Assert.Same(mockHardware.Object, vm.Hardware);
|
|
Assert.Single(vm.Children); // One SensorType group
|
|
|
|
var typeGroup = vm.Children[0];
|
|
Assert.Equal(SensorTreeItemKind.SensorType, typeGroup.Kind);
|
|
Assert.Equal("Temperatures", typeGroup.Text);
|
|
Assert.Single(typeGroup.Children); // One sensor
|
|
|
|
var sensorVm = typeGroup.Children[0];
|
|
Assert.Equal(SensorTreeItemKind.Sensor, sensorVm.Kind);
|
|
Assert.Equal("Core 0", sensorVm.Text);
|
|
Assert.Same(mockSensor.Object, sensorVm.Sensor);
|
|
}
|
|
|
|
[Fact]
|
|
public void Plot_SetsPlotPropertyAndSettings()
|
|
{
|
|
var mockSensor = new Mock<ISensor>();
|
|
mockSensor.Setup(s => s.Identifier).Returns(new Identifier("cpu", "0"));
|
|
|
|
var mockHardware = new Mock<IHardware>();
|
|
mockHardware.Setup(h => h.Identifier).Returns(new Identifier("hw"));
|
|
mockHardware.Setup(h => h.Sensors).Returns(new[] { mockSensor.Object });
|
|
mockHardware.Setup(h => h.SubHardware).Returns(Array.Empty<IHardware>());
|
|
|
|
var settings = CreateMockSettings();
|
|
var vm = SensorTreeItemViewModel.FromHardware(mockHardware.Object, settings);
|
|
|
|
// Root has no sensor, so Plot won't do anything
|
|
vm.Plot = true;
|
|
Assert.False(vm.Plot);
|
|
|
|
// Get SensorVM
|
|
var sensorVm = vm.Children.First().Children.First();
|
|
sensorVm.Plot = true;
|
|
Assert.True(sensorVm.Plot);
|
|
}
|
|
|
|
[Fact]
|
|
public void PenColor_SetsPropertyAndSettings()
|
|
{
|
|
var mockSensor = new Mock<ISensor>();
|
|
mockSensor.Setup(s => s.Identifier).Returns(new Identifier("cpu", "0"));
|
|
|
|
var mockHardware = new Mock<IHardware>();
|
|
mockHardware.Setup(h => h.Sensors).Returns(new[] { mockSensor.Object });
|
|
mockHardware.Setup(h => h.Identifier).Returns(new Identifier("hw"));
|
|
mockHardware.Setup(h => h.SubHardware).Returns(Array.Empty<IHardware>());
|
|
|
|
var settings = CreateMockSettings();
|
|
var vm = SensorTreeItemViewModel.FromHardware(mockHardware.Object, settings);
|
|
var sensorVm = vm.Children.First().Children.First();
|
|
|
|
Assert.Null(sensorVm.PenColor);
|
|
|
|
var color = Color.FromArgb(255, 100, 100, 100);
|
|
sensorVm.PenColor = color;
|
|
Assert.Equal(color, sensorVm.PenColor);
|
|
|
|
sensorVm.PenColor = null;
|
|
Assert.Null(sensorVm.PenColor);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetShowHiddenSensors_UpdatesVisibility()
|
|
{
|
|
var mockSensor = new Mock<ISensor>();
|
|
mockSensor.Setup(s => s.Identifier).Returns(new Identifier("cpu", "0"));
|
|
|
|
var mockHardware = new Mock<IHardware>();
|
|
mockHardware.Setup(h => h.Sensors).Returns(new[] { mockSensor.Object });
|
|
mockHardware.Setup(h => h.Identifier).Returns(new Identifier("hw"));
|
|
mockHardware.Setup(h => h.SubHardware).Returns(Array.Empty<IHardware>());
|
|
|
|
var settings = CreateMockSettings();
|
|
var vm = SensorTreeItemViewModel.FromHardware(mockHardware.Object, settings);
|
|
var typeGroup = vm.Children.First();
|
|
var sensorVm = typeGroup.Children.First();
|
|
|
|
sensorVm.IsVisible = false;
|
|
Assert.Equal(Visibility.Collapsed, sensorVm.RowVisibility);
|
|
|
|
// Hiding the group's only visible sensor now collapses the parent group too (no empty group header left behind).
|
|
Assert.Equal(Visibility.Collapsed, typeGroup.RowVisibility);
|
|
|
|
vm.SetShowHiddenSensors(true);
|
|
|
|
Assert.Equal(Visibility.Visible, sensorVm.RowVisibility);
|
|
Assert.Equal(Visibility.Visible, typeGroup.RowVisibility);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetColumnVisibility_UpdatesAllChildren()
|
|
{
|
|
var root = SensorTreeItemViewModel.CreateRoot("Root");
|
|
|
|
root.SetColumnVisibility(false, true, true);
|
|
|
|
Assert.Equal(Visibility.Collapsed, root.ValueColumnVisibility);
|
|
Assert.Equal(Visibility.Visible, root.MinColumnVisibility);
|
|
Assert.Equal(Visibility.Visible, root.MaxColumnVisibility);
|
|
}
|
|
|
|
[Fact]
|
|
public void RefreshValues_UpdatesProperties()
|
|
{
|
|
var mockSensor = new Mock<ISensor>();
|
|
mockSensor.Setup(s => s.Identifier).Returns(new Identifier("cpu", "0"));
|
|
mockSensor.Setup(s => s.Value).Returns(50f);
|
|
|
|
var mockHardware = new Mock<IHardware>();
|
|
mockHardware.Setup(h => h.Sensors).Returns(new[] { mockSensor.Object });
|
|
mockHardware.Setup(h => h.Identifier).Returns(new Identifier("hw"));
|
|
mockHardware.Setup(h => h.SubHardware).Returns(Array.Empty<IHardware>());
|
|
|
|
var vm = SensorTreeItemViewModel.FromHardware(mockHardware.Object, CreateMockSettings());
|
|
var sensorVm = vm.Children.First().Children.First();
|
|
|
|
mockSensor.Setup(s => s.Value).Returns(60f); // change value
|
|
sensorVm.RefreshValues(); // This triggers PropertyChanged
|
|
|
|
Assert.Contains("60.0", sensorVm.Value);
|
|
}
|
|
}
|