513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
571 lines
24 KiB
C#
571 lines
24 KiB
C#
using System.Text.Json;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
|
|
|
|
public class AlarmTriggerConfigCodecTests
|
|
{
|
|
// ── Parse: ValueMatch ──────────────────────────────────────────────────
|
|
|
|
/// <summary>Verifies that parsing a value-match configuration reads the canonical keys.</summary>
|
|
[Fact]
|
|
public void Parse_ValueMatch_ReadsCanonicalKeys()
|
|
{
|
|
const string json = @"{""attributeName"":""Status"",""matchValue"":""Critical""}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.ValueMatch);
|
|
|
|
Assert.Equal("Status", model.AttributeName);
|
|
Assert.Equal("Critical", model.MatchValue);
|
|
Assert.False(model.NotEquals);
|
|
}
|
|
|
|
/// <summary>Verifies that parsing a value-match configuration accepts the legacy attribute and value keys.</summary>
|
|
[Fact]
|
|
public void Parse_ValueMatch_AcceptsLegacyAttributeAndValueKeys()
|
|
{
|
|
// Older configs used "attribute" and "value" instead of the canonical names.
|
|
const string json = @"{""attribute"":""Status"",""value"":""Critical""}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.ValueMatch);
|
|
|
|
Assert.Equal("Status", model.AttributeName);
|
|
Assert.Equal("Critical", model.MatchValue);
|
|
}
|
|
|
|
/// <summary>Verifies that a not-equals prefix on the match value sets the not-equals flag and is stripped from the value.</summary>
|
|
[Fact]
|
|
public void Parse_ValueMatch_NotEqualsPrefix_SetsFlagAndStripsPrefix()
|
|
{
|
|
const string json = @"{""attributeName"":""Status"",""matchValue"":""!=Good""}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.ValueMatch);
|
|
|
|
Assert.True(model.NotEquals);
|
|
Assert.Equal("Good", model.MatchValue);
|
|
}
|
|
|
|
/// <summary>Verifies that a value-match configuration without a match value leaves it null.</summary>
|
|
[Fact]
|
|
public void Parse_ValueMatch_MissingMatchValue_LeavesNull()
|
|
{
|
|
const string json = @"{""attributeName"":""Status""}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.ValueMatch);
|
|
|
|
Assert.Equal("Status", model.AttributeName);
|
|
Assert.Null(model.MatchValue);
|
|
Assert.False(model.NotEquals);
|
|
}
|
|
|
|
// ── Parse: RangeViolation ──────────────────────────────────────────────
|
|
|
|
/// <summary>Verifies that parsing a range-violation configuration reads the canonical keys.</summary>
|
|
[Fact]
|
|
public void Parse_RangeViolation_ReadsCanonicalKeys()
|
|
{
|
|
const string json = @"{""attributeName"":""Temp"",""min"":0,""max"":100}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RangeViolation);
|
|
|
|
Assert.Equal(0, model.Min);
|
|
Assert.Equal(100, model.Max);
|
|
}
|
|
|
|
/// <summary>Verifies that parsing a range-violation configuration accepts the legacy low and high keys.</summary>
|
|
[Fact]
|
|
public void Parse_RangeViolation_AcceptsLegacyLowHighKeys()
|
|
{
|
|
const string json = @"{""attributeName"":""Temp"",""low"":-10,""high"":50}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RangeViolation);
|
|
|
|
Assert.Equal(-10, model.Min);
|
|
Assert.Equal(50, model.Max);
|
|
}
|
|
|
|
/// <summary>Verifies that canonical range-violation keys take precedence over legacy ones when both are present.</summary>
|
|
[Fact]
|
|
public void Parse_RangeViolation_CanonicalKeysWinOverLegacy()
|
|
{
|
|
// If both canonical and legacy aliases are present, the canonical key wins.
|
|
const string json = @"{""attributeName"":""T"",""min"":0,""low"":-999,""max"":100,""high"":999}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RangeViolation);
|
|
|
|
Assert.Equal(0, model.Min);
|
|
Assert.Equal(100, model.Max);
|
|
}
|
|
|
|
/// <summary>Verifies that numeric values stored as strings in a range-violation configuration are parsed.</summary>
|
|
[Fact]
|
|
public void Parse_RangeViolation_StringNumericValues_AreParsed()
|
|
{
|
|
// Some configs serialize min/max as JSON strings. Codec accepts both.
|
|
const string json = @"{""attributeName"":""T"",""min"":""1.5"",""max"":""9.75""}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RangeViolation);
|
|
|
|
Assert.Equal(1.5, model.Min);
|
|
Assert.Equal(9.75, model.Max);
|
|
}
|
|
|
|
// ── Parse: RateOfChange ────────────────────────────────────────────────
|
|
|
|
/// <summary>Verifies that parsing a rate-of-change configuration reads all of its fields.</summary>
|
|
[Fact]
|
|
public void Parse_RateOfChange_ReadsAllFields()
|
|
{
|
|
const string json = @"{""attributeName"":""Pressure"",""thresholdPerSecond"":25,""windowSeconds"":2,""direction"":""rising""}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RateOfChange);
|
|
|
|
Assert.Equal("Pressure", model.AttributeName);
|
|
Assert.Equal(25, model.ThresholdPerSecond);
|
|
Assert.Equal(2, model.WindowSeconds);
|
|
Assert.Equal("rising", model.Direction);
|
|
}
|
|
|
|
/// <summary>Verifies that direction aliases in a rate-of-change configuration are normalized.</summary>
|
|
/// <param name="input">The direction text as stored in the configuration.</param>
|
|
/// <param name="expected">The expected normalized direction.</param>
|
|
[Theory]
|
|
[InlineData("rising", "rising")]
|
|
[InlineData("Rising", "rising")]
|
|
[InlineData("up", "rising")]
|
|
[InlineData("positive", "rising")]
|
|
[InlineData("falling", "falling")]
|
|
[InlineData("Down", "falling")]
|
|
[InlineData("negative", "falling")]
|
|
[InlineData("either", "either")]
|
|
[InlineData("bogus", "either")]
|
|
[InlineData("", "either")]
|
|
public void Parse_RateOfChange_NormalizesDirectionAliases(string input, string expected)
|
|
{
|
|
var json = $@"{{""attributeName"":""x"",""direction"":""{input}""}}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RateOfChange);
|
|
|
|
Assert.Equal(expected, model.Direction);
|
|
}
|
|
|
|
/// <summary>Verifies that a rate-of-change configuration without a direction defaults to either.</summary>
|
|
[Fact]
|
|
public void Parse_RateOfChange_MissingDirection_DefaultsToEither()
|
|
{
|
|
// Older configs predate the direction field — the codec must default it
|
|
// so existing data round-trips without surprises.
|
|
const string json = @"{""attributeName"":""x"",""thresholdPerSecond"":10,""windowSeconds"":1}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RateOfChange);
|
|
|
|
Assert.Equal("either", model.Direction);
|
|
}
|
|
|
|
// ── Parse: misc ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Verifies that null, empty or whitespace input parses to the default model.</summary>
|
|
/// <param name="input">The null, empty or whitespace configuration text.</param>
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
public void Parse_NullOrWhitespace_ReturnsDefaultModel(string? input)
|
|
{
|
|
var model = AlarmTriggerConfigCodec.Parse(input, AlarmTriggerType.ValueMatch);
|
|
|
|
Assert.Null(model.AttributeName);
|
|
Assert.Null(model.MatchValue);
|
|
Assert.False(model.NotEquals);
|
|
Assert.Equal("either", model.Direction);
|
|
}
|
|
|
|
/// <summary>Verifies that malformed JSON parses to the default model without throwing.</summary>
|
|
[Fact]
|
|
public void Parse_MalformedJson_ReturnsDefaultModel_DoesNotThrow()
|
|
{
|
|
var model = AlarmTriggerConfigCodec.Parse("{not valid", AlarmTriggerType.RangeViolation);
|
|
|
|
Assert.Null(model.Min);
|
|
Assert.Null(model.Max);
|
|
}
|
|
|
|
// ── Serialize: ValueMatch ──────────────────────────────────────────────
|
|
|
|
/// <summary>Verifies that serializing a value-match model writes only the canonical keys.</summary>
|
|
[Fact]
|
|
public void Serialize_ValueMatch_WritesCanonicalKeysOnly()
|
|
{
|
|
var model = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Status",
|
|
MatchValue = "Critical",
|
|
// Foreign fields from other trigger types must NOT leak into the JSON.
|
|
Min = 5,
|
|
ThresholdPerSecond = 99,
|
|
Direction = "rising"
|
|
};
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.ValueMatch);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
var root = doc.RootElement;
|
|
Assert.Equal("Status", root.GetProperty("attributeName").GetString());
|
|
Assert.Equal("Critical", root.GetProperty("matchValue").GetString());
|
|
Assert.False(root.TryGetProperty("min", out _));
|
|
Assert.False(root.TryGetProperty("thresholdPerSecond", out _));
|
|
Assert.False(root.TryGetProperty("direction", out _));
|
|
}
|
|
|
|
/// <summary>Verifies that serializing a not-equals value-match model prepends the not-equals marker to the match value.</summary>
|
|
[Fact]
|
|
public void Serialize_ValueMatch_NotEquals_PrependsBangEqualsToMatchValue()
|
|
{
|
|
var model = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Status",
|
|
MatchValue = "Good",
|
|
NotEquals = true
|
|
};
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.ValueMatch);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
Assert.Equal("!=Good", doc.RootElement.GetProperty("matchValue").GetString());
|
|
}
|
|
|
|
/// <summary>Verifies that a null attribute name is serialized as an empty string.</summary>
|
|
[Fact]
|
|
public void Serialize_ValueMatch_NullAttributeName_WritesEmptyString()
|
|
{
|
|
// AlarmActor uses attributeName for subscription filtering, so the key
|
|
// must always be present even when the user hasn't picked one yet.
|
|
var model = new AlarmTriggerModel { MatchValue = "x" };
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.ValueMatch);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
Assert.Equal("", doc.RootElement.GetProperty("attributeName").GetString());
|
|
}
|
|
|
|
// ── Serialize: RangeViolation ──────────────────────────────────────────
|
|
|
|
/// <summary>Verifies that serializing a range-violation model writes the canonical numeric keys.</summary>
|
|
[Fact]
|
|
public void Serialize_RangeViolation_WritesCanonicalNumericKeys()
|
|
{
|
|
var model = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Temp",
|
|
Min = 0,
|
|
Max = 100,
|
|
MatchValue = "ignored"
|
|
};
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.RangeViolation);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
var root = doc.RootElement;
|
|
Assert.Equal(0, root.GetProperty("min").GetDouble());
|
|
Assert.Equal(100, root.GetProperty("max").GetDouble());
|
|
Assert.False(root.TryGetProperty("matchValue", out _));
|
|
}
|
|
|
|
/// <summary>Verifies that a null range bound is omitted from the serialized output.</summary>
|
|
[Fact]
|
|
public void Serialize_RangeViolation_NullBound_OmitsKey()
|
|
{
|
|
var model = new AlarmTriggerModel { AttributeName = "Temp", Min = 0, Max = null };
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.RangeViolation);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
Assert.True(doc.RootElement.TryGetProperty("min", out _));
|
|
Assert.False(doc.RootElement.TryGetProperty("max", out _));
|
|
}
|
|
|
|
// ── Serialize: RateOfChange ────────────────────────────────────────────
|
|
|
|
/// <summary>Verifies that serializing a rate-of-change model writes the threshold, window and direction.</summary>
|
|
[Fact]
|
|
public void Serialize_RateOfChange_WritesThresholdWindowAndDirection()
|
|
{
|
|
var model = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Pressure",
|
|
ThresholdPerSecond = 25,
|
|
WindowSeconds = 2,
|
|
Direction = "falling"
|
|
};
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.RateOfChange);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
var root = doc.RootElement;
|
|
Assert.Equal(25, root.GetProperty("thresholdPerSecond").GetDouble());
|
|
Assert.Equal(2, root.GetProperty("windowSeconds").GetDouble());
|
|
Assert.Equal("falling", root.GetProperty("direction").GetString());
|
|
}
|
|
|
|
/// <summary>Verifies that serializing a rate-of-change model always includes the direction.</summary>
|
|
[Fact]
|
|
public void Serialize_RateOfChange_AlwaysIncludesDirection()
|
|
{
|
|
// Even with a default-constructed model, the runtime needs to know how
|
|
// to evaluate — direction defaults to "either" and is always emitted.
|
|
var model = new AlarmTriggerModel { AttributeName = "x" };
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.RateOfChange);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
Assert.Equal("either", doc.RootElement.GetProperty("direction").GetString());
|
|
}
|
|
|
|
// ── Round-trip ─────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Verifies that a not-equals value-match model survives a serialize and parse round trip.</summary>
|
|
[Fact]
|
|
public void RoundTrip_ValueMatch_NotEquals_Preserved()
|
|
{
|
|
var original = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Status",
|
|
MatchValue = "Good",
|
|
NotEquals = true
|
|
};
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.ValueMatch);
|
|
var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.ValueMatch);
|
|
|
|
Assert.Equal(original.AttributeName, round.AttributeName);
|
|
Assert.Equal(original.MatchValue, round.MatchValue);
|
|
Assert.True(round.NotEquals);
|
|
}
|
|
|
|
/// <summary>Verifies that a range-violation model survives a serialize and parse round trip.</summary>
|
|
[Fact]
|
|
public void RoundTrip_RangeViolation_Preserved()
|
|
{
|
|
var original = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Temp",
|
|
Min = -10.5,
|
|
Max = 42.25
|
|
};
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.RangeViolation);
|
|
var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RangeViolation);
|
|
|
|
Assert.Equal(original.Min, round.Min);
|
|
Assert.Equal(original.Max, round.Max);
|
|
}
|
|
|
|
/// <summary>Verifies that a rate-of-change model survives a serialize and parse round trip.</summary>
|
|
[Fact]
|
|
public void RoundTrip_RateOfChange_Preserved()
|
|
{
|
|
var original = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Pressure",
|
|
ThresholdPerSecond = 25,
|
|
WindowSeconds = 2,
|
|
Direction = "rising"
|
|
};
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.RateOfChange);
|
|
var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RateOfChange);
|
|
|
|
Assert.Equal(original.AttributeName, round.AttributeName);
|
|
Assert.Equal(original.ThresholdPerSecond, round.ThresholdPerSecond);
|
|
Assert.Equal(original.WindowSeconds, round.WindowSeconds);
|
|
Assert.Equal(original.Direction, round.Direction);
|
|
}
|
|
|
|
// ── Parse: HiLo ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Verifies that parsing a high-low configuration reads all setpoints and priorities.</summary>
|
|
[Fact]
|
|
public void Parse_HiLo_ReadsAllSetpointsAndPriorities()
|
|
{
|
|
const string json = @"{""attributeName"":""Temp"",""loLo"":0,""lo"":10,""hi"":90,""hiHi"":100,""loLoPriority"":900,""loPriority"":500,""hiPriority"":500,""hiHiPriority"":900}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.HiLo);
|
|
|
|
Assert.Equal("Temp", model.AttributeName);
|
|
Assert.Equal(0, model.LoLo);
|
|
Assert.Equal(10, model.Lo);
|
|
Assert.Equal(90, model.Hi);
|
|
Assert.Equal(100, model.HiHi);
|
|
Assert.Equal(900, model.LoLoPriority);
|
|
Assert.Equal(500, model.LoPriority);
|
|
Assert.Equal(500, model.HiPriority);
|
|
Assert.Equal(900, model.HiHiPriority);
|
|
}
|
|
|
|
/// <summary>Verifies that a high-low configuration with only some setpoints parses, leaving the missing ones null.</summary>
|
|
[Fact]
|
|
public void Parse_HiLo_AcceptsPartialSetpoints_MissingOnesAreNull()
|
|
{
|
|
// Common case: only Hi/HiHi configured for over-temp protection.
|
|
const string json = @"{""attributeName"":""Temp"",""hi"":80,""hiHi"":100}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.HiLo);
|
|
|
|
Assert.Null(model.LoLo);
|
|
Assert.Null(model.Lo);
|
|
Assert.Equal(80, model.Hi);
|
|
Assert.Equal(100, model.HiHi);
|
|
Assert.Null(model.HiPriority);
|
|
}
|
|
|
|
// ── Serialize: HiLo ────────────────────────────────────────────────────
|
|
|
|
/// <summary>Verifies that serializing a high-low model omits null setpoints and priorities.</summary>
|
|
[Fact]
|
|
public void Serialize_HiLo_OmitsNullSetpointsAndPriorities()
|
|
{
|
|
var model = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Temp",
|
|
Hi = 80,
|
|
HiHi = 100,
|
|
HiHiPriority = 900
|
|
// Lo, LoLo, and the other priorities left null
|
|
};
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.HiLo);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
var root = doc.RootElement;
|
|
Assert.Equal(80, root.GetProperty("hi").GetDouble());
|
|
Assert.Equal(100, root.GetProperty("hiHi").GetDouble());
|
|
Assert.Equal(900, root.GetProperty("hiHiPriority").GetInt32());
|
|
Assert.False(root.TryGetProperty("lo", out _));
|
|
Assert.False(root.TryGetProperty("loLo", out _));
|
|
Assert.False(root.TryGetProperty("hiPriority", out _));
|
|
Assert.False(root.TryGetProperty("loPriority", out _));
|
|
}
|
|
|
|
/// <summary>Verifies that serializing a high-low model does not leak fields that belong to other trigger types.</summary>
|
|
[Fact]
|
|
public void Serialize_HiLo_DoesNotLeakForeignTriggerTypeFields()
|
|
{
|
|
// matchValue, min/max, threshold/window/direction must NOT show up in
|
|
// HiLo output even if the model happens to carry them.
|
|
var model = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Temp",
|
|
Hi = 80,
|
|
MatchValue = "ignored",
|
|
Min = 1,
|
|
ThresholdPerSecond = 99,
|
|
Direction = "rising"
|
|
};
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.HiLo);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
var root = doc.RootElement;
|
|
Assert.False(root.TryGetProperty("matchValue", out _));
|
|
Assert.False(root.TryGetProperty("min", out _));
|
|
Assert.False(root.TryGetProperty("thresholdPerSecond", out _));
|
|
Assert.False(root.TryGetProperty("direction", out _));
|
|
}
|
|
|
|
/// <summary>Verifies that parsing a high-low configuration reads the deadbands.</summary>
|
|
[Fact]
|
|
public void Parse_HiLo_ReadsDeadbands()
|
|
{
|
|
const string json = @"{""attributeName"":""Temp"",""hi"":80,""hiHi"":100,""hiDeadband"":2,""hiHiDeadband"":5}";
|
|
|
|
var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.HiLo);
|
|
|
|
Assert.Equal(2, model.HiDeadband);
|
|
Assert.Equal(5, model.HiHiDeadband);
|
|
Assert.Null(model.LoDeadband);
|
|
Assert.Null(model.LoLoDeadband);
|
|
}
|
|
|
|
/// <summary>Verifies that serializing a high-low model omits null deadbands.</summary>
|
|
[Fact]
|
|
public void Serialize_HiLo_OmitsNullDeadbands()
|
|
{
|
|
var model = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Temp",
|
|
Hi = 80,
|
|
HiDeadband = 2
|
|
// HiHiDeadband / LoDeadband / LoLoDeadband null
|
|
};
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.HiLo);
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
Assert.Equal(2, doc.RootElement.GetProperty("hiDeadband").GetDouble());
|
|
Assert.False(doc.RootElement.TryGetProperty("hiHiDeadband", out _));
|
|
Assert.False(doc.RootElement.TryGetProperty("loDeadband", out _));
|
|
}
|
|
|
|
/// <summary>Verifies that a high-low model preserves all of its fields through a serialize and parse round trip.</summary>
|
|
[Fact]
|
|
public void RoundTrip_HiLo_PreservesAllFields()
|
|
{
|
|
var original = new AlarmTriggerModel
|
|
{
|
|
AttributeName = "Pressure",
|
|
LoLo = -5,
|
|
Lo = 0,
|
|
Hi = 90,
|
|
HiHi = 110,
|
|
LoLoPriority = 800,
|
|
LoPriority = 400,
|
|
HiPriority = 400,
|
|
HiHiPriority = 800,
|
|
LoLoDeadband = 1,
|
|
LoDeadband = 2,
|
|
HiDeadband = 3,
|
|
HiHiDeadband = 4
|
|
};
|
|
|
|
var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.HiLo);
|
|
var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.HiLo);
|
|
|
|
Assert.Equal(original.AttributeName, round.AttributeName);
|
|
Assert.Equal(original.LoLo, round.LoLo);
|
|
Assert.Equal(original.Lo, round.Lo);
|
|
Assert.Equal(original.Hi, round.Hi);
|
|
Assert.Equal(original.HiHi, round.HiHi);
|
|
Assert.Equal(original.LoLoPriority, round.LoLoPriority);
|
|
Assert.Equal(original.LoPriority, round.LoPriority);
|
|
Assert.Equal(original.HiPriority, round.HiPriority);
|
|
Assert.Equal(original.HiHiPriority, round.HiHiPriority);
|
|
Assert.Equal(original.LoLoDeadband, round.LoLoDeadband);
|
|
Assert.Equal(original.LoDeadband, round.LoDeadband);
|
|
Assert.Equal(original.HiDeadband, round.HiDeadband);
|
|
Assert.Equal(original.HiHiDeadband, round.HiHiDeadband);
|
|
}
|
|
|
|
// ── NormalizeDirection (direct) ────────────────────────────────────────
|
|
|
|
/// <summary>Verifies that direction normalization handles every alias and falls back to either for unknown input.</summary>
|
|
/// <param name="input">The direction text to normalize.</param>
|
|
/// <param name="expected">The expected normalized direction.</param>
|
|
[Theory]
|
|
[InlineData("rising", "rising")]
|
|
[InlineData("RISING", "rising")]
|
|
[InlineData("falling", "falling")]
|
|
[InlineData("up", "rising")]
|
|
[InlineData("down", "falling")]
|
|
[InlineData("positive", "rising")]
|
|
[InlineData("negative", "falling")]
|
|
[InlineData("either", "either")]
|
|
[InlineData("", "either")]
|
|
[InlineData(null, "either")]
|
|
[InlineData("nonsense", "either")]
|
|
public void NormalizeDirection_HandlesAllAliasesAndFallsBackToEither(string? input, string expected)
|
|
{
|
|
Assert.Equal(expected, AlarmTriggerConfigCodec.NormalizeDirection(input));
|
|
}
|
|
}
|