Add characterization tests; consolidate SensorFormatter switches

Establishes a safety net before refactoring, and lands the first
behavior-preserving cleanup.

Tests (no production behavior change):
- SensorFormatter: full per-type coverage of GetFormatString, FormatValue,
  and GetPlotUnit across every SensorType.
- RemoteWebServer: extract testable seams (ResolveRoute, VerifyCredentials,
  internal pure helpers) and pin routing (incl. the no-hijack rule), query
  and Prometheus parsing, JSON/metric shape, credential semantics, and the
  legacy SHA-256 vector.
- HardwareMonitorService: pin the enable-flag -> settings-key mapping.
- Logger: add a TimeProvider/base-dir test seam; deterministic rotation tests.
- Add [InternalsVisibleTo] for the test project.

Cleanup:
- Collapse SensorFormatter's three parallel SensorType switches into one
  GetFormat source of truth; dedupe CelsiusToFahrenheit. Verified identical
  by the new characterization tests.

167 tests pass (was 55).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 19:58:44 -05:00
co-authored by Claude Opus 4.8
parent 54a88e1f31
commit 463e80709f
8 changed files with 778 additions and 146 deletions
@@ -32,6 +32,97 @@ public class SensorFormatterTests
Assert.Equal("{0:F1}", SensorFormatter.GetFormatString(mockSensor.Object));
}
// Characterization: pins the GetFormatString mapping for every SensorType so the
// three parallel SensorType switches can be safely collapsed into one lookup table.
[Theory]
[InlineData(SensorType.Voltage, "{0:F3} V")]
[InlineData(SensorType.Current, "{0:F3} A")]
[InlineData(SensorType.Clock, "{0:F1} MHz")]
[InlineData(SensorType.Load, "{0:F1} %")]
[InlineData(SensorType.Temperature, "{0:F1} °C")]
[InlineData(SensorType.Fan, "{0:F0} RPM")]
[InlineData(SensorType.Flow, "{0:F1} L/h")]
[InlineData(SensorType.Control, "{0:F1} %")]
[InlineData(SensorType.Level, "{0:F1} %")]
[InlineData(SensorType.Power, "{0:F1} W")]
[InlineData(SensorType.Data, "{0:F1} GB")]
[InlineData(SensorType.SmallData, "{0:F1} MB")]
[InlineData(SensorType.Factor, "{0:F3}")]
[InlineData(SensorType.Frequency, "{0:F1} Hz")]
[InlineData(SensorType.Throughput, "{0:F1} B/s")]
[InlineData(SensorType.TimeSpan, "{0:g}")]
[InlineData(SensorType.Timing, "{0:F3} ns")]
[InlineData(SensorType.Energy, "{0:F0} mWh")]
[InlineData(SensorType.Noise, "{0:F0} dBA")]
[InlineData(SensorType.Conductivity, "{0:F1} µS/cm")]
[InlineData(SensorType.Humidity, "{0:F0} %")]
[InlineData((SensorType)999, "{0:F1}")]
public void GetFormatString_AllSensorTypes(SensorType sensorType, string expected)
{
var mockSensor = new Mock<ISensor>();
mockSensor.Setup(s => s.SensorType).Returns(sensorType);
Assert.Equal(expected, SensorFormatter.GetFormatString(mockSensor.Object));
}
// Characterization: pins the GetPlotUnit mapping for every SensorType (Celsius).
[Theory]
[InlineData(SensorType.Voltage, "V")]
[InlineData(SensorType.Current, "A")]
[InlineData(SensorType.Clock, "MHz")]
[InlineData(SensorType.Load, "%")]
[InlineData(SensorType.Temperature, "°C")]
[InlineData(SensorType.Fan, "RPM")]
[InlineData(SensorType.Flow, "L/h")]
[InlineData(SensorType.Control, "%")]
[InlineData(SensorType.Level, "%")]
[InlineData(SensorType.Power, "W")]
[InlineData(SensorType.Data, "GB")]
[InlineData(SensorType.SmallData, "MB")]
[InlineData(SensorType.Factor, "1")]
[InlineData(SensorType.Frequency, "Hz")]
[InlineData(SensorType.Throughput, "B/s")]
[InlineData(SensorType.TimeSpan, "s")]
[InlineData(SensorType.Timing, "ns")]
[InlineData(SensorType.Energy, "mWh")]
[InlineData(SensorType.Noise, "dBA")]
[InlineData(SensorType.Conductivity, "µS/cm")]
[InlineData(SensorType.Humidity, "%")]
[InlineData((SensorType)999, "")]
public void GetPlotUnit_AllSensorTypes_Celsius(SensorType sensorType, string expected)
{
Assert.Equal(expected, SensorFormatter.GetPlotUnit(sensorType, TemperatureUnit.Celsius));
}
// Characterization: pins FormatValue for the "regular" {0:Fn} unit types (Celsius).
// Throughput, TimeSpan, and Temperature-Fahrenheit have their own dedicated tests.
[Theory]
[InlineData(SensorType.Voltage, 1.234f, "1.234 V")]
[InlineData(SensorType.Current, 2.5f, "2.500 A")]
[InlineData(SensorType.Clock, 3500f, "3500.0 MHz")]
[InlineData(SensorType.Load, 42.5f, "42.5 %")]
[InlineData(SensorType.Temperature, 50.5f, "50.5 °C")]
[InlineData(SensorType.Fan, 1200f, "1200 RPM")]
[InlineData(SensorType.Flow, 10.5f, "10.5 L/h")]
[InlineData(SensorType.Control, 75.5f, "75.5 %")]
[InlineData(SensorType.Level, 60.5f, "60.5 %")]
[InlineData(SensorType.Power, 95.5f, "95.5 W")]
[InlineData(SensorType.Data, 8.5f, "8.5 GB")]
[InlineData(SensorType.SmallData, 256.5f, "256.5 MB")]
[InlineData(SensorType.Factor, 1.234f, "1.234")]
[InlineData(SensorType.Frequency, 60.5f, "60.5 Hz")]
[InlineData(SensorType.Timing, 1.234f, "1.234 ns")]
[InlineData(SensorType.Energy, 1500f, "1500 mWh")]
[InlineData(SensorType.Noise, 45f, "45 dBA")]
[InlineData(SensorType.Conductivity, 12.5f, "12.5 µS/cm")]
[InlineData(SensorType.Humidity, 55f, "55 %")]
[InlineData((SensorType)999, 12.34f, "12.3")]
public void FormatValue_RegularTypes_Celsius(SensorType sensorType, float value, string expected)
{
var mockSensor = new Mock<ISensor>();
mockSensor.Setup(s => s.SensorType).Returns(sensorType);
Assert.Equal(expected, SensorFormatter.FormatValue(mockSensor.Object, value, TemperatureUnit.Celsius));
}
[Fact]
public void FormatValue_NullValue_ReturnsDash()
{