Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/L5kParserTests.cs
Joseph Doherty e3c0750f7d Auto: abcip-2.6 — AOI input/output handling
AOI-aware browse paths: AOI instances now fan out under directional
sub-folders (Inputs/, Outputs/, InOut/) instead of a flat layout. The
sub-folders only appear when at least one member carries a non-Local
AoiQualifier, so plain UDT tags keep the pre-2.6 flat structure.

- Add AoiQualifier enum (Local / Input / Output / InOut) + new property
  on AbCipStructureMember (defaults to Local).
- L5K parser learns ADD_ON_INSTRUCTION_DEFINITION blocks; PARAMETER
  entries' Usage attribute flows through L5kMember.Usage.
- L5X parser captures the Usage attribute on <Parameter> elements.
- L5kIngest maps Usage strings (Input/Output/InOut) to AoiQualifier;
  null + unknown values map to Local.
- AbCipDriver.DiscoverAsync groups directional members under
  Inputs / Outputs / InOut sub-folders when any member is non-Local.
- Tests for L5K AOI block parsing, L5X Usage capture, ingest mapping
  (both formats), and AOI-vs-plain UDT discovery fan-out.

Closes #234
2026-04-25 18:58:49 -04:00

199 lines
6.6 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip.Import;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
[Trait("Category", "Unit")]
public sealed class L5kParserTests
{
[Fact]
public void Controller_scope_TAG_block_parses_name_datatype_externalaccess()
{
const string body = """
TAG
Motor1_Speed : DINT (Description := "Motor 1 set point", ExternalAccess := Read/Write) := 0;
Tank_Level : REAL (ExternalAccess := Read Only) := 0.0;
END_TAG
""";
var doc = L5kParser.Parse(new StringL5kSource(body));
doc.Tags.Count.ShouldBe(2);
doc.Tags[0].Name.ShouldBe("Motor1_Speed");
doc.Tags[0].DataType.ShouldBe("DINT");
doc.Tags[0].ProgramScope.ShouldBeNull();
doc.Tags[0].ExternalAccess.ShouldBe("Read/Write");
doc.Tags[0].Description.ShouldBe("Motor 1 set point");
doc.Tags[0].AliasFor.ShouldBeNull();
doc.Tags[1].Name.ShouldBe("Tank_Level");
doc.Tags[1].DataType.ShouldBe("REAL");
doc.Tags[1].ExternalAccess.ShouldBe("Read Only");
}
[Fact]
public void Program_scope_TAG_block_carries_program_name()
{
const string body = """
PROGRAM MainProgram (Class := Standard)
TAG
StepIndex : DINT := 0;
Running : BOOL := 0;
END_TAG
END_PROGRAM
""";
var doc = L5kParser.Parse(new StringL5kSource(body));
doc.Tags.Count.ShouldBe(2);
doc.Tags.ShouldAllBe(t => t.ProgramScope == "MainProgram");
doc.Tags.Select(t => t.Name).ShouldBe(["StepIndex", "Running"]);
}
[Fact]
public void Alias_tag_is_flagged()
{
const string body = """
TAG
Motor1 : DINT := 0;
Motor1_Alias : DINT (AliasFor := "Motor1", ExternalAccess := Read/Write);
END_TAG
""";
var doc = L5kParser.Parse(new StringL5kSource(body));
var alias = doc.Tags.Single(t => t.Name == "Motor1_Alias");
alias.AliasFor.ShouldBe("Motor1");
}
[Fact]
public void DATATYPE_block_collects_member_lines()
{
const string body = """
DATATYPE TankUDT (FamilyType := NoFamily)
MEMBER Level : REAL (ExternalAccess := Read/Write) := 0.0;
MEMBER Pressure : REAL := 0.0;
MEMBER Active : BOOL := 0;
END_DATATYPE
""";
var doc = L5kParser.Parse(new StringL5kSource(body));
doc.DataTypes.Count.ShouldBe(1);
var udt = doc.DataTypes[0];
udt.Name.ShouldBe("TankUDT");
udt.Members.Count.ShouldBe(3);
udt.Members[0].Name.ShouldBe("Level");
udt.Members[0].DataType.ShouldBe("REAL");
udt.Members[0].ExternalAccess.ShouldBe("Read/Write");
udt.Members[1].Name.ShouldBe("Pressure");
udt.Members[2].Name.ShouldBe("Active");
udt.Members[2].DataType.ShouldBe("BOOL");
}
[Fact]
public void DATATYPE_member_with_array_dim_keeps_type_clean()
{
const string body = """
DATATYPE BatchUDT
MEMBER Recipe : DINT[16] := 0;
MEMBER Name : STRING := "";
END_DATATYPE
""";
var doc = L5kParser.Parse(new StringL5kSource(body));
var udt = doc.DataTypes[0];
var recipe = udt.Members.First(m => m.Name == "Recipe");
recipe.DataType.ShouldBe("DINT");
recipe.ArrayDim.ShouldBe(16);
var nameMember = udt.Members.First(m => m.Name == "Name");
nameMember.DataType.ShouldBe("STRING");
nameMember.ArrayDim.ShouldBeNull();
}
[Fact]
public void Block_comments_are_stripped_before_parsing()
{
const string body = """
(* This is a long
multi-line comment with TAG and END_TAG inside, parser must skip *)
TAG
Real_Tag : DINT := 0;
END_TAG
""";
var doc = L5kParser.Parse(new StringL5kSource(body));
doc.Tags.Count.ShouldBe(1);
doc.Tags[0].Name.ShouldBe("Real_Tag");
}
[Fact]
public void Unknown_sections_are_skipped_silently()
{
const string body = """
CONFIG SomeConfig (Class := Standard)
ConfigData := 0;
END_CONFIG
MOTION_GROUP Motion1
Member := whatever;
END_MOTION_GROUP
TAG
Real_Tag : DINT := 0;
END_TAG
""";
var doc = L5kParser.Parse(new StringL5kSource(body));
doc.Tags.Count.ShouldBe(1);
doc.Tags[0].Name.ShouldBe("Real_Tag");
}
[Fact]
public void AOI_definition_block_collects_parameters_with_Usage()
{
// PR abcip-2.6 — ADD_ON_INSTRUCTION_DEFINITION blocks with PARAMETER entries carrying
// Usage := Input / Output / InOut. The parser surfaces them as L5kDataType members so
// AOI-typed tags pick up a layout the same way UDT-typed tags do.
const string body = """
ADD_ON_INSTRUCTION_DEFINITION MyValveAoi (Revision := "1.0")
PARAMETERS
PARAMETER Cmd : BOOL (Usage := Input) := 0;
PARAMETER Status : DINT (Usage := Output, ExternalAccess := Read Only) := 0;
PARAMETER Buffer : DINT (Usage := InOut) := 0;
PARAMETER Internal : DINT := 0;
END_PARAMETERS
LOCAL_TAGS
Working : DINT := 0;
END_LOCAL_TAGS
END_ADD_ON_INSTRUCTION_DEFINITION
""";
var doc = L5kParser.Parse(new StringL5kSource(body));
var aoi = doc.DataTypes.Single(d => d.Name == "MyValveAoi");
aoi.Members.Count.ShouldBe(4);
aoi.Members.Single(m => m.Name == "Cmd").Usage.ShouldBe("Input");
aoi.Members.Single(m => m.Name == "Status").Usage.ShouldBe("Output");
aoi.Members.Single(m => m.Name == "Buffer").Usage.ShouldBe("InOut");
aoi.Members.Single(m => m.Name == "Internal").Usage.ShouldBeNull();
}
[Fact]
public void Multi_line_TAG_entry_is_concatenated()
{
const string body = """
TAG
Motor1 : DINT (Description := "Long description spanning",
ExternalAccess := Read/Write) := 0;
END_TAG
""";
var doc = L5kParser.Parse(new StringL5kSource(body));
doc.Tags.Count.ShouldBe(1);
doc.Tags[0].Description.ShouldBe("Long description spanning");
doc.Tags[0].ExternalAccess.ShouldBe("Read/Write");
}
}