Merge pull request '[twincat] TwinCAT — ENUM and ALIAS at discovery' (#345) from auto/twincat/1.5 into auto/driver-gaps

This commit was merged in pull request #345.
This commit is contained in:
2026-04-25 17:51:08 -04:00
2 changed files with 175 additions and 1 deletions

View File

@@ -272,12 +272,50 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
foreach (ISymbol symbol in loader.Symbols)
{
if (cancellationToken.IsCancellationRequested) yield break;
var mapped = MapSymbolTypeName(symbol.DataType?.Name);
var mapped = ResolveSymbolDataType(symbol.DataType);
var readOnly = !IsSymbolWritable(symbol);
yield return new TwinCATDiscoveredSymbol(symbol.InstancePath, mapped, readOnly);
}
}
/// <summary>
/// Resolve an IEC atomic <see cref="TwinCATDataType"/> for a TwinCAT symbol's data type.
/// ENUMs surface as their underlying integer (the enum's <c>BaseType</c>); ALIAS chains
/// are walked recursively via <see cref="IAliasType.BaseType"/> until an atomic primitive
/// is reached. POINTER / REFERENCE / INTERFACE / UNION / STRUCT / FB / array types remain
/// out of scope and surface as <c>null</c> so the caller skips them.
/// </summary>
/// <remarks>
/// Recursion is bounded at <see cref="MaxAliasDepth"/> as a defence against pathological
/// cycles in the type graph — TwinCAT shouldn't emit those, but this is cheap insurance.
/// </remarks>
internal const int MaxAliasDepth = 16;
internal static TwinCATDataType? ResolveSymbolDataType(IDataType? dataType)
{
var current = dataType;
for (var depth = 0; current is not null && depth < MaxAliasDepth; depth++)
{
switch (current.Category)
{
case DataTypeCategory.Primitive:
case DataTypeCategory.String:
return MapSymbolTypeName(current.Name);
case DataTypeCategory.Enum:
case DataTypeCategory.Alias:
// IEnumType : IAliasType, so BaseType walk handles both. For an enum the
// base type is the underlying integer; for alias chains it's the next link.
if (current is IAliasType alias) { current = alias.BaseType; continue; }
return null;
default:
// POINTER / REFERENCE / INTERFACE / UNION / STRUCT / ARRAY / FB / Program —
// explicitly out of scope at this PR.
return null;
}
}
return null;
}
private static TwinCATDataType? MapSymbolTypeName(string? typeName) => typeName switch
{
"BOOL" or "BIT" => TwinCATDataType.Bool,

View File

@@ -0,0 +1,136 @@
using Shouldly;
using TwinCAT.Ads.TypeSystem;
using TwinCAT.TypeSystem;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
/// <summary>
/// Coverage for <see cref="AdsTwinCATClient.ResolveSymbolDataType"/> — the discovery
/// helper that maps a TwinCAT <see cref="IDataType"/> to a driver <see cref="TwinCATDataType"/>.
/// PR-1.5 added ENUM and ALIAS chain handling on top of the original primitive name match.
/// </summary>
[Trait("Category", "Unit")]
public sealed class TwinCATTypeResolutionTests
{
[Fact]
public void Primitive_resolves_directly()
{
var dt = new PrimitiveType("DINT", typeof(int));
AdsTwinCATClient.ResolveSymbolDataType(dt).ShouldBe(TwinCATDataType.DInt);
}
[Fact]
public void Null_data_type_returns_null()
{
AdsTwinCATClient.ResolveSymbolDataType(null).ShouldBeNull();
}
[Fact]
public void Single_alias_resolves_to_underlying_atomic()
{
var primitive = new PrimitiveType("INT", typeof(short));
var alias = new AliasType("DegreesC", primitive);
AdsTwinCATClient.ResolveSymbolDataType(alias).ShouldBe(TwinCATDataType.Int);
}
[Fact]
public void Chained_alias_resolves_through_multiple_links()
{
var primitive = new PrimitiveType("DINT", typeof(int));
var inner = new AliasType("Inner", primitive);
var outer = new AliasType("Outer", inner);
AdsTwinCATClient.ResolveSymbolDataType(outer).ShouldBe(TwinCATDataType.DInt);
}
[Fact]
public void Alias_to_unknown_primitive_returns_null()
{
// A primitive whose IEC name we don't recognise should drop through cleanly. Pick a CLR
// type with a known marshal size so PrimitiveType's ctor accepts it; the name is what
// exercises the unknown-type branch in MapSymbolTypeName.
var primitive = new PrimitiveType("MYSTERY", typeof(int));
var alias = new AliasType("Wrap", primitive);
AdsTwinCATClient.ResolveSymbolDataType(alias).ShouldBeNull();
}
[Fact]
public void Enum_resolves_to_underlying_integer_base_type()
{
var underlying = new PrimitiveType("INT", typeof(short));
var enumType = new TestEnumType("EState", underlying);
AdsTwinCATClient.ResolveSymbolDataType(enumType).ShouldBe(TwinCATDataType.Int);
}
[Fact]
public void Enum_with_dint_base_resolves_to_DInt()
{
var underlying = new PrimitiveType("DINT", typeof(int));
var enumType = new TestEnumType("EBigFlags", underlying);
AdsTwinCATClient.ResolveSymbolDataType(enumType).ShouldBe(TwinCATDataType.DInt);
}
[Fact]
public void Cyclic_alias_chain_terminates_at_depth_cap()
{
// Pathological: a self-referential alias. The resolver must give up rather than spin.
var loop = new SelfReferentialAlias();
AdsTwinCATClient.ResolveSymbolDataType(loop).ShouldBeNull();
}
/// <summary>
/// Minimal in-test stub for <see cref="IEnumType"/>. Only <c>Category</c> and
/// <c>BaseType</c> are exercised by <see cref="AdsTwinCATClient.ResolveSymbolDataType"/>;
/// the rest of the surface throws to flag any accidental dependence in future tests.
/// </summary>
private sealed class TestEnumType(string name, IDataType baseType) : IDataType, IAliasType
{
public string Name { get; } = name;
public IDataType BaseType { get; } = baseType;
public DataTypeCategory Category => DataTypeCategory.Enum;
public string BaseTypeName => BaseType.Name;
public string FullName => Name;
public string Namespace => string.Empty;
public int Id => 0;
public string Comment => string.Empty;
public ITypeAttributeCollection Attributes => throw new NotSupportedException();
public bool IsContainer => false;
public bool IsPointer => false;
public bool IsReference => false;
public bool IsPrimitive => false;
public int Size => BaseType.Size;
public int ByteSize => BaseType.ByteSize;
public int BitSize => BaseType.BitSize;
public bool IsBitType => false;
public bool IsByteAligned => true;
}
/// <summary>Self-referential alias — exercises the depth cap.</summary>
private sealed class SelfReferentialAlias : IDataType, IAliasType
{
public string Name => "Loop";
public IDataType BaseType => this;
public DataTypeCategory Category => DataTypeCategory.Alias;
public string BaseTypeName => Name;
public string FullName => Name;
public string Namespace => string.Empty;
public int Id => 0;
public string Comment => string.Empty;
public ITypeAttributeCollection Attributes => throw new NotSupportedException();
public bool IsContainer => false;
public bool IsPointer => false;
public bool IsReference => false;
public bool IsPrimitive => false;
public int Size => 0;
public int ByteSize => 0;
public int BitSize => 0;
public bool IsBitType => false;
public bool IsByteAligned => true;
}
}