Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/LooseValueCodecTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
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.
2026-10-07 15:05:49 -04:00

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&lt;object?&gt;</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());
}
}