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:
+127
@@ -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" });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user