PR 5.2 — Browse + read parity scenarios
Three scenarios using ParityHarness.RequireBoth: - Discover_emits_same_variable_set_for_both_backends — symmetric set diff on the full-reference set must be empty. - Discover_emits_same_DataType_and_SecurityClass_per_attribute — meta triple (DriverDataType, SecurityClass, IsHistorized) must match per attribute. - Read_returns_same_value_and_status_for_a_sampled_attribute — samples the first 5 discovered variables, reads through both backends, asserts StatusCode equality and value-CLR-type equality (raw values may drift between the two reads on a live Galaxy). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests;
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.2 — Browse + read parity. Discovers the address space through both
|
||||
/// backends and asserts the surface they expose matches: same folder set,
|
||||
/// same variable set, same DataType / SecurityClass / IsHistorized flags.
|
||||
/// Then reads a sample of resolved variables and diffs the snapshot triplets.
|
||||
/// </summary>
|
||||
[Trait("Category", "ParityE2E")]
|
||||
[Collection(nameof(ParityCollection))]
|
||||
public sealed class BrowseAndReadParityTests
|
||||
{
|
||||
private readonly ParityHarness _h;
|
||||
public BrowseAndReadParityTests(ParityHarness h) => _h = h;
|
||||
|
||||
[Fact]
|
||||
public async Task Discover_emits_same_variable_set_for_both_backends()
|
||||
{
|
||||
_h.RequireBoth();
|
||||
|
||||
var snapshots = await _h.RunOnAvailableAsync(async (driver, ct) =>
|
||||
{
|
||||
var b = new RecordingAddressSpaceBuilder();
|
||||
await ((ITagDiscovery)driver).DiscoverAsync(b, ct);
|
||||
return b;
|
||||
}, CancellationToken.None);
|
||||
|
||||
var legacy = snapshots[ParityHarness.Backend.LegacyHost];
|
||||
var mxgw = snapshots[ParityHarness.Backend.MxGateway];
|
||||
|
||||
var legacyRefs = legacy.Variables.Select(v => v.AttributeInfo.FullName)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var mxgwRefs = mxgw.Variables.Select(v => v.AttributeInfo.FullName)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Symmetric difference must be empty — the in-process driver and the legacy
|
||||
// proxy walk the same Galaxy ZB hierarchy, so their full-reference sets
|
||||
// must agree exactly.
|
||||
legacyRefs.Except(mxgwRefs, StringComparer.OrdinalIgnoreCase).ShouldBeEmpty();
|
||||
mxgwRefs.Except(legacyRefs, StringComparer.OrdinalIgnoreCase).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Discover_emits_same_DataType_and_SecurityClass_per_attribute()
|
||||
{
|
||||
_h.RequireBoth();
|
||||
|
||||
var snapshots = await _h.RunOnAvailableAsync(async (driver, ct) =>
|
||||
{
|
||||
var b = new RecordingAddressSpaceBuilder();
|
||||
await ((ITagDiscovery)driver).DiscoverAsync(b, ct);
|
||||
return b.Variables.ToDictionary(
|
||||
v => v.AttributeInfo.FullName,
|
||||
v => (v.AttributeInfo.DriverDataType, v.AttributeInfo.SecurityClass, v.AttributeInfo.IsHistorized),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}, CancellationToken.None);
|
||||
|
||||
var legacy = snapshots[ParityHarness.Backend.LegacyHost];
|
||||
var mxgw = snapshots[ParityHarness.Backend.MxGateway];
|
||||
|
||||
foreach (var kvp in legacy)
|
||||
{
|
||||
var fullRef = kvp.Key;
|
||||
mxgw.ShouldContainKey(fullRef);
|
||||
mxgw[fullRef].ShouldBe(kvp.Value,
|
||||
$"DataType/SecurityClass/IsHistorized must match for '{fullRef}'");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_returns_same_value_and_status_for_a_sampled_attribute()
|
||||
{
|
||||
_h.RequireBoth();
|
||||
|
||||
// Discover via the legacy backend, pick a sample, then read the same address
|
||||
// through both backends. We sample a small handful so the test stays fast and
|
||||
// doesn't hammer ZB / the gateway.
|
||||
var b = new RecordingAddressSpaceBuilder();
|
||||
await ((ITagDiscovery)_h.LegacyDriver!).DiscoverAsync(b, CancellationToken.None);
|
||||
|
||||
var sample = b.Variables.Take(5).Select(v => v.AttributeInfo.FullName).ToArray();
|
||||
if (sample.Length == 0) Assert.Skip("dev Galaxy has no discoverable variables");
|
||||
|
||||
var reads = await _h.RunOnAvailableAsync(
|
||||
(driver, ct) => ((IReadable)driver).ReadAsync(sample, ct),
|
||||
CancellationToken.None);
|
||||
|
||||
var legacyReads = reads[ParityHarness.Backend.LegacyHost];
|
||||
var mxgwReads = reads[ParityHarness.Backend.MxGateway];
|
||||
|
||||
legacyReads.Count.ShouldBe(sample.Length);
|
||||
mxgwReads.Count.ShouldBe(sample.Length);
|
||||
|
||||
for (var i = 0; i < sample.Length; i++)
|
||||
{
|
||||
// Status codes must agree on a per-tag basis. Values may legitimately differ
|
||||
// when the dev Galaxy is live (a setpoint can change between the two reads),
|
||||
// so we accept structural equality on type rather than value equality.
|
||||
(mxgwReads[i].StatusCode == legacyReads[i].StatusCode).ShouldBeTrue(
|
||||
$"StatusCode parity for '{sample[i]}': legacy=0x{legacyReads[i].StatusCode:X8}, mxgw=0x{mxgwReads[i].StatusCode:X8}");
|
||||
(mxgwReads[i].Value?.GetType() ?? typeof(object))
|
||||
.ShouldBe(legacyReads[i].Value?.GetType() ?? typeof(object),
|
||||
$"value CLR type parity for '{sample[i]}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user