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.
158 lines
5.9 KiB
C#
158 lines
5.9 KiB
C#
using System.Text.Json;
|
|
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Communication.Tests;
|
|
|
|
/// <summary>
|
|
/// Goldens for <see cref="LooseValueCodec"/> — the type-tagged carrier for the
|
|
/// <c>object?</c> members of the site command plane (script parameters and
|
|
/// return values, attribute values, tag reads/writes).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The contract these tests pin down is that a boxed value keeps its RUNTIME
|
|
/// CLR TYPE across the wire, not merely its printed form. Today's Akka JSON
|
|
/// serializer preserves it, and an operator-facing tag value that silently
|
|
/// turns from <c>int</c> into <c>long</c> (or from <c>DateTime</c> into a
|
|
/// UTC-normalised copy) is a behaviour change, not a refactor.
|
|
/// </remarks>
|
|
public class LooseValueCodecTests
|
|
{
|
|
/// <summary>Provides scalar values of each supported CLR type for the round-trip theory.</summary>
|
|
/// <returns>The scalar test values, one per row.</returns>
|
|
public static IEnumerable<object[]> TaggedScalars() =>
|
|
[
|
|
[true],
|
|
[false],
|
|
[42],
|
|
[-1],
|
|
[long.MaxValue],
|
|
[3.5d],
|
|
[1.5f],
|
|
["a string"],
|
|
[string.Empty],
|
|
[12.3456789m],
|
|
[new DateTime(2026, 7, 22, 1, 2, 3, DateTimeKind.Utc)],
|
|
[new DateTimeOffset(2026, 7, 22, 1, 2, 3, TimeSpan.FromHours(-5))],
|
|
[Guid.Parse("11111111-2222-3333-4444-555555555555")],
|
|
[TimeSpan.FromMinutes(90)],
|
|
[new byte[] { 1, 2, 3 }]
|
|
];
|
|
|
|
/// <summary>Verifies that a tagged value keeps its runtime CLR type after a round trip through the codec.</summary>
|
|
/// <param name="value">The boxed value to round-trip.</param>
|
|
[Theory]
|
|
[MemberData(nameof(TaggedScalars))]
|
|
public void TaggedValues_KeepTheirClrType(object value)
|
|
{
|
|
var restored = LooseValueCodec.FromProto(LooseValueCodec.ToProto(value));
|
|
|
|
Assert.NotNull(restored);
|
|
Assert.Equal(value.GetType(), restored.GetType());
|
|
StructuralEquality.AssertDeepEqual(value, restored, value.GetType().Name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A local <see cref="DateTime"/> must come back local, and an offset
|
|
/// <see cref="DateTimeOffset"/> must come back with the same offset — the
|
|
/// reason these ride as round-trip strings instead of proto timestamps.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DateValues_KeepKindAndOffset()
|
|
{
|
|
var local = new DateTime(2026, 7, 22, 1, 2, 3, DateTimeKind.Local);
|
|
var offset = new DateTimeOffset(2026, 7, 22, 1, 2, 3, TimeSpan.FromHours(5.5));
|
|
|
|
var restoredLocal = Assert.IsType<DateTime>(LooseValueCodec.FromProto(LooseValueCodec.ToProto(local)));
|
|
var restoredOffset =
|
|
Assert.IsType<DateTimeOffset>(LooseValueCodec.FromProto(LooseValueCodec.ToProto(offset)));
|
|
|
|
Assert.Equal(DateTimeKind.Local, restoredLocal.Kind);
|
|
Assert.Equal(local, restoredLocal);
|
|
Assert.Equal(TimeSpan.FromHours(5.5), restoredOffset.Offset);
|
|
}
|
|
|
|
/// <summary>An unset carrier and an explicit null marker both decode to null.</summary>
|
|
[Fact]
|
|
public void NullIsCarriedTwoWays()
|
|
{
|
|
Assert.Null(LooseValueCodec.FromProto(null));
|
|
Assert.Null(LooseValueCodec.FromProto(LooseValueCodec.ToProto(null)));
|
|
Assert.Null(LooseValueCodec.ToProtoOrNull(null));
|
|
}
|
|
|
|
/// <summary>
|
|
/// A dictionary entry whose value is null stays distinct from an absent key —
|
|
/// the reason <c>LooseNull</c> exists at all.
|
|
/// </summary>
|
|
[Fact]
|
|
public void NullMapValue_SurvivesAsAPresentKey()
|
|
{
|
|
var original = new Dictionary<string, object?> { ["present"] = null };
|
|
|
|
var restored = LooseValueCodec.FromProtoMap(LooseValueCodec.ToProtoMap(original));
|
|
|
|
Assert.True(restored.ContainsKey("present"));
|
|
Assert.Null(restored["present"]);
|
|
}
|
|
|
|
/// <summary>A null dictionary stays null; an empty one stays empty.</summary>
|
|
[Fact]
|
|
public void NullMap_StaysDistinctFromEmptyMap()
|
|
{
|
|
Assert.Null(LooseValueCodec.ToProtoMapOrNull(null));
|
|
Assert.Null(LooseValueCodec.FromProtoMapOrNull(null));
|
|
|
|
var empty = LooseValueCodec.FromProtoMapOrNull(
|
|
LooseValueCodec.ToProtoMapOrNull(new Dictionary<string, object?>()));
|
|
|
|
Assert.NotNull(empty);
|
|
Assert.Empty(empty);
|
|
}
|
|
|
|
/// <summary>Nested lists and maps recurse, so element values keep their types too.</summary>
|
|
[Fact]
|
|
public void NestedCollections_KeepElementTypes()
|
|
{
|
|
object original = new List<object?>
|
|
{
|
|
1,
|
|
"two",
|
|
null,
|
|
new Dictionary<string, object?> { ["deep"] = 3.5d }
|
|
};
|
|
|
|
var restored = LooseValueCodec.FromProto(LooseValueCodec.ToProto(original));
|
|
|
|
StructuralEquality.AssertDeepEqual(original, restored, "list");
|
|
}
|
|
|
|
/// <summary>
|
|
/// The documented widening: a typed collection keeps its ELEMENTS' types but
|
|
/// the container comes back as <c>List<object?></c>. Recorded here so
|
|
/// the behaviour is a decision rather than a surprise.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TypedList_WidensToObjectList()
|
|
{
|
|
var restored = LooseValueCodec.FromProto(LooseValueCodec.ToProto(new List<int> { 1, 2, 3 }));
|
|
|
|
var list = Assert.IsType<List<object?>>(restored);
|
|
Assert.Equal([1, 2, 3], list.Cast<int>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// The documented lossy escape hatch: a value outside the tagged set keeps its
|
|
/// DATA but not its CLR identity — it decodes as a <see cref="JsonElement"/>.
|
|
/// Nothing on the command plane carries such a value today; the fallback
|
|
/// exists so an unexpected one cannot fault a command.
|
|
/// </summary>
|
|
[Fact]
|
|
public void UntaggedValue_FallsBackToJsonAndLosesItsClrType()
|
|
{
|
|
var restored = LooseValueCodec.FromProto(LooseValueCodec.ToProto((byte)7));
|
|
|
|
var element = Assert.IsType<JsonElement>(restored);
|
|
Assert.Equal("7", element.GetRawText());
|
|
}
|
|
}
|