feat(abcip): thread nested-struct template id so nested UDT members are addressable (#6)
This commit is contained in:
@@ -227,6 +227,66 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
byName["Motor1.Running"].DriverDataType.ShouldBe(DriverDataType.Boolean);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PRODUCTION PATH (no test seam): a controller-discovered UDT whose Template Object carries
|
||||
/// a NESTED struct member fans the nested member out into its own atomic leaves WITHOUT any
|
||||
/// <c>SeedDiscoveredUdtShapeForTest</c>. The nested member block carries the nested UDT's
|
||||
/// template instance id (low 12 bits, struct flag set); the decoder captures it as
|
||||
/// <c>NestedTemplateId</c> and DiscoverAsync threads it into a real <c>FetchUdtShapeAsync</c>
|
||||
/// for the nested id. Proves the nested fetch actually happened (reader consulted for the
|
||||
/// nested id) and that the nested struct's leaf member is addressable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Controller_discovered_UDT_nested_struct_expands_via_nested_template_id_fetch_no_seam()
|
||||
{
|
||||
const string device = "ab://10.0.0.5/1,0";
|
||||
const uint motorTemplateId = 0x2A; // parent UDT template id
|
||||
const uint statusTemplateId = 0x123; // nested UDT template id — exceeds a byte (12-bit proof)
|
||||
|
||||
var builder = new RecordingBuilder();
|
||||
var enumeratorFactory = new FakeEnumeratorFactory(
|
||||
new AbCipDiscoveredTag("Motor1", null, AbCipDataType.Structure, ReadOnly: false,
|
||||
TemplateInstanceId: motorTemplateId));
|
||||
|
||||
// The parent template carries an atomic member (Speed/Real) + a NESTED struct member (Status)
|
||||
// whose member-info encodes the struct flag (0x8000) | the nested template id (0x123). The
|
||||
// nested template id is read off @udt/0x123 and yields one atomic leaf (Code/DInt).
|
||||
var templateReaderFactory = new IdKeyedTemplateReaderFactory
|
||||
{
|
||||
Templates =
|
||||
{
|
||||
[motorTemplateId] = BuildSimpleTemplate("MotorUdt", 12,
|
||||
("Speed", 0xCA, 0, 0), // Real
|
||||
("Status", (ushort)(0x8000 | statusTemplateId), 0, 8)), // struct + nested id 0x123
|
||||
[statusTemplateId] = BuildSimpleTemplate("StatusUdt", 4,
|
||||
("Code", 0xC4, 0, 0)), // DInt
|
||||
},
|
||||
};
|
||||
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(device)],
|
||||
EnableControllerBrowse = true,
|
||||
}, "drv-1", enumeratorFactory: enumeratorFactory, templateReaderFactory: templateReaderFactory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
await drv.DiscoverAsync(builder, CancellationToken.None);
|
||||
|
||||
// The nested template id actually drove a fetch — the reader was consulted for it (the seam
|
||||
// would have left LastTemplateId at the parent id and never read the nested shape).
|
||||
templateReaderFactory.RequestedIds.ShouldContain(statusTemplateId);
|
||||
|
||||
var byName = builder.Variables.ToDictionary(v => v.Info.FullName, v => v.Info);
|
||||
// The parent atomic leaf is emitted.
|
||||
byName.ShouldContainKey("Motor1.Speed");
|
||||
byName["Motor1.Speed"].DriverDataType.ShouldBe(DriverDataType.Float32);
|
||||
// The NESTED struct's atomic leaf is addressable — proving the nested fetch + fan-out ran.
|
||||
byName.ShouldContainKey("Motor1.Status.Code");
|
||||
byName["Motor1.Status.Code"].DriverDataType.ShouldBe(DriverDataType.Int32);
|
||||
// A Motor1.Status sub-folder was materialised for the nested struct.
|
||||
builder.Folders.ShouldContain(f => f.BrowseName == "Motor1.Status");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A discovered Structure whose shape cannot be resolved degrades to the prior single
|
||||
/// Variable behavior (no regression): one Variable under the Discovered folder keyed by
|
||||
@@ -619,6 +679,8 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
public Dictionary<uint, byte[]> Templates { get; } = new();
|
||||
/// <summary>Gets the last template id any reader was asked for.</summary>
|
||||
public uint? LastTemplateId { get; private set; }
|
||||
/// <summary>Gets every template id any reader was asked for, in request order.</summary>
|
||||
public List<uint> RequestedIds { get; } = new();
|
||||
|
||||
/// <summary>Creates a reader over the shared template map.</summary>
|
||||
public IAbCipTemplateReader Create() => new Reader(this);
|
||||
@@ -629,6 +691,7 @@ public sealed class AbCipDriverDiscoveryTests
|
||||
AbCipTagCreateParams deviceParams, uint templateInstanceId, CancellationToken cancellationToken)
|
||||
{
|
||||
outer.LastTemplateId = templateInstanceId;
|
||||
outer.RequestedIds.Add(templateInstanceId);
|
||||
return Task.FromResult(
|
||||
outer.Templates.TryGetValue(templateInstanceId, out var blob) ? blob : []);
|
||||
}
|
||||
|
||||
@@ -86,6 +86,31 @@ public sealed class CipTemplateObjectDecoderTests
|
||||
shape.Members.Single().DataType.ShouldBe(AbCipDataType.Structure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A struct member's lower 12 bits carry the nested UDT's template instance id (same
|
||||
/// encoding as the Symbol Object); the decoder captures it as <c>NestedTemplateId</c> so the
|
||||
/// nested shape can be fetched. A scalar member carries no nested id. Uses the FULL 12-bit
|
||||
/// mask (0x123 here exceeds a byte) to prove the id is not byte-truncated.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Struct_member_captures_nested_template_id_scalar_member_does_not()
|
||||
{
|
||||
var bytes = BuildTemplate("ContainerUdt", instanceSize: 8,
|
||||
("Inner", info: (ushort)(0x8000 | 0x123), arraySize: 0, offset: 0), // struct + nested id 0x123
|
||||
("Count", info: 0xC4, arraySize: 0, offset: 4)); // scalar DINT, no nested id
|
||||
|
||||
var shape = CipTemplateObjectDecoder.Decode(bytes);
|
||||
|
||||
shape.ShouldNotBeNull();
|
||||
shape.Members.Count.ShouldBe(2);
|
||||
shape.Members[0].Name.ShouldBe("Inner");
|
||||
shape.Members[0].DataType.ShouldBe(AbCipDataType.Structure);
|
||||
shape.Members[0].NestedTemplateId.ShouldBe(0x123u); // full 12-bit id, not byte-truncated
|
||||
shape.Members[1].Name.ShouldBe("Count");
|
||||
shape.Members[1].DataType.ShouldBe(AbCipDataType.DInt);
|
||||
shape.Members[1].NestedTemplateId.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that array members carry correct non-one array lengths.</summary>
|
||||
[Fact]
|
||||
public void Array_member_carries_non_one_ArrayLength()
|
||||
|
||||
Reference in New Issue
Block a user