review(Runtime): record findings + fix artifact-decode type tolerance

Review at HEAD 7286d320. Runtime-002/006 (Medium): DeploymentArtifact lenient-parse
now degrades wrong-typed JSON fields to defaults/skipped-row instead of throwing (fails
the deploy) + regression tests; byte-parity with AddressSpaceComposer preserved. Runtime-001
(UNS rename) deferred cross-module (needs AddressSpacePlan rename signal + EnsureFolder
rename). 003/004/005 Won't-Fix.
This commit is contained in:
Joseph Doherty
2026-06-19 10:52:22 -04:00
parent 5aaa82bc26
commit 3512089c90
3 changed files with 388 additions and 21 deletions
@@ -0,0 +1,127 @@
using System;
using System.Linq;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary>
/// Regression coverage for Runtime-002: the artifact-decode is documented as lenient — "empty /
/// malformed blobs return an empty list / empty composition". A blob that is syntactically valid JSON
/// but carries a field of the WRONG JSON type (e.g. a string Enabled, a numeric Name) must NOT throw
/// <see cref="InvalidOperationException"/> out of the parse (the old code called GetString()/GetBoolean()
/// unguarded). It must degrade to the field default / a skipped row instead.
/// </summary>
public sealed class DeploymentArtifactMalformedTypeTests
{
private static byte[] BlobOf(object snapshot) =>
System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(snapshot);
/// <summary>A string-typed Enabled ("true") must not throw; the row should still decode (default Enabled=true).</summary>
[Fact]
public void ParseDriverInstances_string_typed_Enabled_does_not_throw()
{
var blob = BlobOf(new
{
DriverInstances = new[]
{
new
{
DriverInstanceRowId = Guid.NewGuid(),
DriverInstanceId = "d1",
Name = "drv-1",
DriverType = "Modbus",
Enabled = "true", // WRONG TYPE: string, not bool
DriverConfig = "{}",
},
},
});
var specs = Should.NotThrow(() => DeploymentArtifact.ParseDriverInstances(blob));
specs.Count.ShouldBe(1);
// A non-bool Enabled degrades to the field default (Enabled = true / "desired here").
specs[0].Enabled.ShouldBeTrue();
specs[0].DriverInstanceId.ShouldBe("d1");
}
/// <summary>A numeric-typed Name / DriverType must not throw; a row with an unusable required field
/// (DriverType missing/wrong) is skipped, a row whose only wrong field is the optional Name keeps decoding.</summary>
[Fact]
public void ParseDriverInstances_numeric_typed_Name_does_not_throw()
{
var blob = BlobOf(new
{
DriverInstances = new[]
{
new
{
DriverInstanceRowId = (object)Guid.NewGuid(),
DriverInstanceId = (object)"d1",
Name = (object)5, // WRONG TYPE: number, not string
DriverType = (object)"Modbus",
Enabled = (object)true,
DriverConfig = (object)"{}",
},
},
});
var specs = Should.NotThrow(() => DeploymentArtifact.ParseDriverInstances(blob));
specs.Count.ShouldBe(1);
// A non-string Name degrades to null → Name falls back to the DriverInstanceId.
specs[0].Name.ShouldBe("d1");
}
/// <summary>The composition decode must also tolerate a wrong-typed UNS field (numeric UnsAreaId)
/// without throwing — it degrades to skipping that row.</summary>
[Fact]
public void ParseComposition_numeric_typed_UnsAreaId_does_not_throw()
{
var blob = BlobOf(new
{
UnsAreas = new object[]
{
new { UnsAreaId = 42, Name = "Area-numeric" }, // WRONG TYPE: numeric id → skipped
new { UnsAreaId = "area-ok", Name = "Area-OK" },
},
});
var composition = Should.NotThrow(() => DeploymentArtifact.ParseComposition(blob));
// The well-typed area survives; the numeric-id one is skipped (id resolves to null → dropped).
composition.UnsAreas.Select(a => a.UnsAreaId).ShouldBe(new[] { "area-ok" });
}
/// <summary>A driver row whose DriverType is the wrong type is skipped (required field unusable),
/// not a parse failure.</summary>
[Fact]
public void ParseDriverInstances_numeric_typed_DriverType_skips_row_without_throwing()
{
var blob = BlobOf(new
{
DriverInstances = new object[]
{
new
{
DriverInstanceRowId = (object)Guid.NewGuid(),
DriverInstanceId = (object)"bad",
Name = (object)"drv-bad",
DriverType = (object)7, // WRONG TYPE: numeric → unusable required field
Enabled = (object)true,
DriverConfig = (object)"{}",
},
new
{
DriverInstanceRowId = (object)Guid.NewGuid(),
DriverInstanceId = (object)"good",
Name = (object)"drv-good",
DriverType = (object)"Modbus",
Enabled = (object)true,
DriverConfig = (object)"{}",
},
},
});
var specs = Should.NotThrow(() => DeploymentArtifact.ParseDriverInstances(blob));
specs.Select(s => s.DriverInstanceId).ShouldBe(new[] { "good" });
}
}