Resolve TwinCAT symbol data types via the IDataType chain instead of a flat name match. ALIAS chains walk BaseType recursively (depth-capped at 16 against pathological cycles); ENUM surfaces its underlying integer base type. POINTER / REFERENCE / INTERFACE / UNION / STRUCT / ARRAY / FB remain explicitly out of scope and surface as null. Closes #309
137 lines
5.1 KiB
C#
137 lines
5.1 KiB
C#
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;
|
|
}
|
|
}
|