Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATTypeResolutionTests.cs
Joseph Doherty 0df14ab94a Auto: twincat-1.5 — ENUM/ALIAS discovery
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
2026-04-25 17:48:45 -04:00

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;
}
}