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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user